Search code examples
javascriptsvggridquadraticcubic-bezier

Subdivision of Four-Sided, 2D Shape


I'm looking for an approach to splitting a four sided shape into a grid. For example: enter image description here

Ultimately I need to be able to convert the resulting shapes to SVG, but I'm happy to handle conversion to/from another library or coordinate system. What I'm looking for is how to approach the calculation.

Assume the shape is a four-sided shape drawn quadratically where each side can be concave or convex, but no edges overlap with other edges or themselves and any of the four sides can be curved.

The same approach for a four-sided polygon (shape with straight edges is trivial), and if two opposed edges are straight lines, is is easy to find the intersecting points because they will lie along straight lines drawn between the subdivisions of the opposing sides. From there is is relatively easy to calculate the curve needed to join them to the previous point along the alternative axis:

enter image description here

However when there are not two straight, opposed sides (as in the third example above) I am unsure of how to find the points because there is no longer the certainty of the points lying along a straight line.

I've spent a long time looking for a documented approach, but to no avail.

Here is an example of the kind of starting shape using SVG to describe it (it doesn't have to processed in SVG, as long as I can output to SVG.

<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
     viewBox="0 0 406.4 233.4" xml:space="preserve">
  <path class="st0" d="M394.3,232.7c-106-37.8-353.7,0-353.7,0s-90.4-151.2,0-207.3s353.7,0,353.7,0S420.3,154.7,394.3,232.7z"/>
</svg>

EDIT: I asked a similar question over at Stack Exchange Maths and one of the answers describes one approach - the use of a Coons Patch. Quora explaination here.


Solution

  • You can see the live example and full code on Codepen here.

    Data representation

    The simplest data representation of the image below uses Cubic Bézier curves. I believe that would also cover all of your use cases. In order not to pollute our code with various special cases, we will require the input to always be in the format of four subsequent Cubic Bézier curves as if we drew them. This means we cannot use:

    • Quadractic Bézier curves (convertible to Cubic by mirroring the other control point)
    • Segments (convertible to Cubic Bézier curve by placing the control points equidistantly between the endpoints on the line)
    • Close path [Z SVG command] (convertible to Cubic Bézier curve by calculating the given segment and then taking it from there)

    More on the subject of paths in SVG

    pure shape

    Its SVG representation

    <path d=" M50 50
         C 100 100 400 100 450 50
         C 475 250 475 250 450 450
         C 250 300 250 300 50 450
         C 150 100 150 250 50 50"
     fill="transparent"
     stroke="black"
    />
    

    However, for convenience we will define our own datastructures.

    Point is just a plain old Vector2D class.

    class Point {
      constructor (x, y) {
        this.x = x
        this.y = y
      }
    }
    

    Curve is Cubic Bézier curve.

    cubic bézier

    class Curve {
      constructor (
        startPointX, startPointY,
        controlPointAX, controlPointAY,
        controlPointBX, controlPointBY,
        endPointX, endPointY) {
        this.start = new Point(startPointX, startPointY)
        this.controlA = new Point(controlPointAX, controlPointAY)
        this.controlB = new Point(controlPointBX, controlPointBY)
        this.end = new Point(endPointX, endPointY)
      }
    
    }
    

    Grid is just a container for the curves.

    class Grid {
      constructor (topSide, rightSide, bottomSide, leftSide, horizontalCuts, verticalCuts) {
        this.topSide = topSide
        this.rightSide = rightSide
        this.bottomSide = bottomSide
        this.leftSide = leftSide
    
        // These define how we want to slice our shape. Just ignore them for now
        this.verticalCuts = verticalCuts
        this.horizontalCuts = horizontalCuts
      }
    }
    

    Let's fill it with the same shape.

    let grid = new Grid(
      new Curve(50, 50, 100, 100, 400, 100, 450, 50),
      new Curve(450, 50, 475, 250, 475, 250, 450, 450),
      new Curve(450, 450, 250, 300, 250, 300, 50, 450),
      new Curve(50, 450, 150, 100, 150, 250, 50, 50),
      8,
      6
    )
    

    Finding intersection points

    intersection points

    Apparently you already implemented it using the t approach (as opposed to true curve splice length) so I am putting it here just for completness' sake.

    Note that cuts is the actual number of the intersection points (red points) you'll get. That is, the starting point and ending point are not there (but with minor edits to cut() they can be)

    function cut (cuts, callback) {
      cuts++
      for (let j = 1; j < cuts; j++) {
        const t = j / cuts
        callback(t)
      }
    }
    
    class Curve {
    
    // ...
    
      getIntersectionPoints (cuts) {
        let points = []
        cut(cuts, (t) => {
          points.push(new Point(this.x(t), this.y(t)))
        })
        return points
      }
    
      x (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.start.x +
                3 * ((1 - t) * (1 - t)) * t * this.controlA.x +
                3 * (1 - t) * (t * t) * this.controlB.x +
                (t * t * t) * this.end.x
      }
    
      y (t) {
        return ((1 - t) * (1 - t) * (1 - t)) * this.start.y +
                3 * ((1 - t) * (1 - t)) * t * this.controlA.y +
                3 * (1 - t) * (t * t) * this.controlB.y +
                (t * t * t) * this.end.y
      }
    }
    

    Finding the splitting curves

    function lerp (from, to, t) {
      return from * (1.0 - t) + (to * t)
    }
    
    class Curve {
    
    // ...
    
      getSplitCurves (cuts, oppositeCurve, fromCurve, toCurve) {
        let curves = []
        cut(cuts, (t) => {
          let start = new Point(this.x(t), this.y(t))
          // NOTE1: We must go backwards
          let end = new Point(oppositeCurve.x(1 - t), oppositeCurve.y(1 - t))
          let controlA = new Point(
            // NOTE2: Interpolate control points
            lerp(fromCurve.controlA.x, toCurve.controlA.x, t),
            lerp(fromCurve.controlA.y, toCurve.controlA.y, t)
          )
          let controlB = new Point(
            // NOTE2: Interpolate control points
            lerp(fromCurve.controlB.x, toCurve.controlB.x, t),
            lerp(fromCurve.controlB.y, toCurve.controlB.y, t)
          )
          curves.push(new Curve(
            start.x, start.y,
            controlA.x, controlA.y,
            controlB.x, controlB.y,
            end.x, end.y
          ))
        })
        return curves
      }
    }
    

    There are some fishy things with the code above.

    NOTE1: Since the curves are represented in the order you draw them, the opposing sides face different directions. For instance, the top side is drawn left-to-right, but the bottom one right-to-left. Maybe an image will help:

    order of endpoints

    NOTE2: This is how we get the control points for the Béziers splitting the shape. t interpolation on the segment connecting the control points of opposing sides.

    Here are those segments. Their endpoints are the control points of respective curves.

    control point segments Inkscape screenshot

    This is the final result when we render the curves: grid

    You can see the live example and full code on Codepen here.

    Where to go from here

    More intersections

    This obviously isn't the final result. We still need to find the intersection points of the generated curves. However, finding the intersections of two Bezier curves is non-trivial. Here's a nice StackOverflow answer on the topic which will lead you to this neat library that will do the heavy lifting for you (look at the code of bezier3bezier3() and you'll understand)

    Splitting the curves

    Once you find the intersection points, you will want to find at which t they occur. Why t you ask? So you can split the curve.

    The actual final result

    At the end you'll need to pick those fractions of the curves and arrange them to represent individual fields of the grid.

    As you can see, you still have a long journey to go, I only went a fraction of it (and still managed to write a lengthy answer :D ).