0

I working with UIBezierPath and shape detection. For painting im using "UIPanGestureRecognizer".

Example of code:

My shape definder

var gesture = UIPanGestureRecognizer()
.
.
.
view.addGestureRecognizer(gesture.onChange { \[weak self\] gesture in
let point = gesture.location(in: self.view)
let shapeL = CAShapeLayer()
shapeL.strokeColor = UIColor.black.cgColor
shapeL.lineWidth = 2
shapeL.fillColor = UIColor.clear.cgColor

        switch gesture.state {
        case .began:
            //some code
        currentBezierPath = UIBezierPath()
            break
        case .changed:
            //some code
        shapeLayer.path = self.currentBezierPath.cgPath
            break
        case .ended:
            //define what user was painted(circle, rectangle, etc)
        shapeDefinder(path: currentBezierPath)
            break
        default:
            break
        })

shapeDefinder

func shapeDefinder(path: UIBezierPath) {
if(path.hasFourRightAngles()){
// square 
}

}

extension hasFourRightAngles

extension UIBezierPath {
    func hasFourRightAngles() -> Bool {
        guard self.currentPoint != .zero else {
            // empty path cannot have angles
            return false
        }
    
        let bounds = self.bounds
        let points = [
            bounds.origin,
            CGPoint(x: bounds.minX, y: bounds.minY),
            CGPoint(x: bounds.maxX, y: bounds.maxY),
            CGPoint(x: bounds.minX, y: bounds.maxY)
        ]
        let angleTolerance = 5.0 // in degrees
        var rightAngleCount = 0
        for i in 0...3 {
            let p1 = points[i]
            let p2 = points[(i+1)%4]
            let p3 = points[(i+2)%4]
            let angle = p2.angle(between: p1, and: p3)
            if abs(angle - 90) <= angleTolerance {
                rightAngleCount += 1
            }
        }
        return rightAngleCount >= 4
    }
}

and

extension CGPoint {
func angle(between p1: CGPoint, and p2: CGPoint) -\> CGFloat {
let dx1 = self.x - p1.x
let dy1 = self.y - p1.y
let dx2 = p2.x - self.x
let dy2 = p2.y - self.y
let dotProduct = dx1*dx2 + dy1*dy2
let crossProduct = dx1*dy2 - dx2*dy1
return atan2(crossProduct, dotProduct) \* 180 / .pi
}
}

but my method hasFourRightAngles() doesnt work, it always has true. Cant understand how i can detect square(the user must draw exactly a square, if the user draws a circle, then the check should not pass.) Maybe someone know about some library which works with UIBezierPath for detect shapes?

2 Answers2

2

The bounds of a path are always a rectangle, no matter the shape, so you should expect this function to always return true. From the docs:

The value in this property represents the smallest rectangle that completely encloses all points in the path, including any control points for Bézier and quadratic curves.

If you want to consider the components of the path itself, you'd need to iterate over its components using it's CGPath. See applyWithBlock for how to get the elements. That said, this probably won't work very well, since you likely don't care precisely how the shape was drawn. If you go down this road, you'll probably want to do some work to simplify the curve first, and perhaps put the stokes in a useful order.

If the drawing pattern itself is the important thing (i.e. the user's gesture is what matters), then I would probably keep track of whether this could be a rectangle at each point of the drawing. Either it needs to be roughly colinear to the previous line, or roughly normal. And then the final point must be close to the original point.

The better approach is possibly to consider the final image of the shape, regardless of how it was drawn, and then classify it. For various algorithms to do that, see How to identify different objects in an image?

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
1

Your code to get the bounds of your path will not let you tell if the lines inside the path make right angles. As Rob says in his answer, the bounding box of a path will always be a rectangle, so your current test will always return true.

The bounding box of a circle will be a square, as will the box of any shape who's horizontal and vertical maxima and minima are equal.

It is possible to interrogate the internal elements of the underlying CGPath and look for a series of lines that make a square. I suggest searching for code that parses the elements of a CGPath.

Note that if you are checking freehand drawing, you will likely need some "slop" in your calculations to allow for shapes that are close to, but not exactly squares, or you will likely never find a perfect square.

Also, what if the path contains a square plus other shape elements? You will need to decide how to handle situations like that.

Duncan C
  • 128,072
  • 22
  • 173
  • 272