1

I have a set of 2D Cartesian points [b], which proceed clockwise from the start and form a closed shape. Each one of these has its own companion 2D Cartesian points q0 and q1 which define the Beziér curve around the point (along with the preceding and succeeding points). Together, all these points define a closed 2D composite Beziér curve.

I have a separate point p which is an arbitrary 2D Cartesian point on the same plane. Is there a simple algorithm for finding the (x, y) coordinates of a new 2D Cartesian point q which is the closest point on the path to p?

Four blue points labeled b[0] through b[4], each with two child green points labeled b[n].q0 and b[n].q1 connected to their blue parent by grey lines, forming a red path whose basic shape is dictated by the positions of the blue points, and curvature by the green ones. Above the curve there lies an orange point p, connected by a grey line to a purple point q, which lies on the red path at the closest point to p.

As illustrated here, I have the points b[0] to b[3] and their handles b[n].q0 and b[n].q1, and I have the arbitrary point p. I'm trying to calculate the point q, not as a floating-point position along the curve, but as a pair of (x, y) coordinates.

I tried searching this, but some seemed to only be for a very small curve, others were way over my head with abstract mathematics and scientific research papers.

Any help that leads me toward an algorithmic solution is greatly appreciated, especially if it can be translated into a C-like language rather than the pure math in the above SO answers.

Community
  • 1
  • 1
Ky -
  • 30,724
  • 51
  • 192
  • 308
  • that might not meet the requirements of this site - you could try codereview.stackexchange.com but you'll need to put some working code up! – kafka Jul 21 '16 at 19:29
  • @kafka I'm not asking to have my code reviewed; I'm trying to find an algorithm that will solve my problem. How can I change this question to best represent that? – Ky - Jul 21 '16 at 19:32
  • @aioobe Thank you for your edit, but my problem is that I can't wrap my head around pure math answers and need a code answer to help me understand, and to help solve my problem it needs to translate to the C-like language I'm using. – Ky - Jul 21 '16 at 19:34
  • It's a programming forum, so hopefully you'll get something nicer than a pure math answer. You could add something like "Thankful for any hints, especially pseudo code or description of an algorithm." – aioobe Jul 21 '16 at 19:37
  • @aioobe Hope that's better :) – Ky - Jul 21 '16 at 19:45
  • The "small curve" answer looks like it would give you a point for each segment; run it 4 times for each of the 4 segments and pick whichever is closest. – Mark Ransom Jul 21 '16 at 19:45
  • @MarkRansom The problem with that one is it gives me a `double`, and no sense of where that corresponds to on the cartesian plane. – Ky - Jul 21 '16 at 20:41
  • 1
    The formula for converting the double `t` to `x` and `y` coordinates is in the middle of the code. – Mark Ransom Jul 21 '16 at 20:50
  • beware about asking for "the closest point" in a 2D setting when dealing with parametric curves: it is entirely possible to get two or even more points that are equidistant to your new point `p`. – Mike 'Pomax' Kamermans Jul 29 '16 at 23:52
  • @Mike'Pomax'Kamermans Great point, but for my work it's fine to just get the first one found :) – Ky - Aug 01 '16 at 17:07

1 Answers1

0

By adapting the algorithm posted by Tatarize, I came up with this solution in Swift, which should be translatable to other languages:

struct BezierPoint {
    let q0: CGPoint
    let point: CGPoint
    let q1: CGPoint
}

struct SimpleBezierCurve {
    let left: BezierPoint
    let right: BezierPoint
}

class BezierPath {
    var pathPoints = [BezierPoint]()

    func findClosestPoint(to targetPoint: CGPoint) -> CGPoint {
        let segments = allSegments()
        guard segments.count > 0 else { return targetPoint }
        var closestPoint = (distance: CGFloat.infinity, point: CGPoint(x: CGFloat.infinity, y: CGFloat.infinity))
        segments.forEach{ curve in
            let thisPoint = BezierPath.findClosestPoint(to: targetPoint, along: curve)
            let distance = findDistance(from: targetPoint, to: thisPoint)

            if (distance < closestPoint.distance) {
                closestPoint = (distance: distance, point: thisPoint)
            }
        }
        return closestPoint.point
    }

    func allSegments() -> [SimpleBezierCurve] {
        guard pathPoints.count > 0 else { return [] }
        var segments = [SimpleBezierCurve]()
        var prevPoint = pathPoints[0]
        for i in 1 ..< pathPoints.count {
            let thisPoint = pathPoints[i]
            segments.append(SimpleBezierCurve(left: prevPoint, right: thisPoint))
            prevPoint = thisPoint
        }
        segments.append(SimpleBezierCurve(left: prevPoint, right: pathPoints[0]))
        return segments
    }

    static func findClosestPoint(to point: CGPoint, along curve: SimpleBezierCurve) -> CGPoint {
        return findClosestPointToCubicBezier(to: point, slices: 10, iterations: 10, along: curve)
    }

    // Adapted from https://stackoverflow.com/a/34520607/3939277
    static func findClosestPointToCubicBezier(to target: CGPoint, slices: Int, iterations: Int, along curve: SimpleBezierCurve) -> CGPoint {
        return findClosestPointToCubicBezier(iterations: iterations, to: target, start: 0, end: 1, slices: slices, along: curve)
    }

    // Adapted from https://stackoverflow.com/a/34520607/3939277
    private static func findClosestPointToCubicBezier(iterations iterations: Int, to: CGPoint, start: CGFloat, end: CGFloat, slices: Int, along curve: SimpleBezierCurve) -> CGPoint {
        if iterations <= 0 {
            let position = (start + end) / 2
            let point = self.point(for: position, along: curve)
            return point
        }
        let tick = (end - start) / slices
        var best = CGFloat(0)
        var bestDistance = CGFloat.infinity
        var currentDistance: CGFloat
        var t = start

        while (t <= end) {
            //B(t) = (1-t)**3 p0 + 3(1 - t)**2 t P1 + 3(1-t)t**2 P2 + t**3 P3
            let point = self.point(for: t, along: curve)

            var dx = point.x - to.x;
            var dy = point.y - to.y;
            dx *= dx;
            dy *= dy;
            currentDistance = dx + dy;
            if (currentDistance < bestDistance) {
                bestDistance = currentDistance;
                best = t;
            }
            t += tick;
        }
        return findClosestPointToCubicBezier(iterations: iterations - 1, to: to, start: max(best - tick, 0.0), end: min(best + tick, 1.0), slices: slices, along: curve);
    }

    static func point(for t: CGFloat, along curve: SimpleBezierCurve) -> CGPoint {
        // This had to be broken up to avoid the "Expression too complex" error

        let x0 = curve.left.point.x
        let x1 = curve.left.q1.x
        let x2 = curve.right.q0.x
        let x3 = curve.right.point.x

        let y0 = curve.left.point.y
        let y1 = curve.left.q1.y
        let y2 = curve.right.q0.y
        let y3 = curve.right.point.y

        let x = (1 - t) * (1 - t) * (1 - t) * x0 + 3 * (1 - t) * (1 - t) * t * x1 + 3 * (1 - t) * t * t * x2 + t * t * t * x3
        let y = (1 - t) * (1 - t) * (1 - t) * y0 + 3 * (1 - t) * (1 - t) * t * y1 + 3 * (1 - t) * t * t * y2 + t * t * t * y3

        return CGPoint(x: x, y: y)
    }
}

// Possibly in another file
func findDistance(from a: CGPoint, to b: CGPoint) -> CGFloat {
    return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2));
}

GitHub Gist

Ky -
  • 30,724
  • 51
  • 192
  • 308