Подразделение четырехсторонней формы 2D

Я ищу подход к разделению четырехсторонней формы в сетку. Например: enter image description here

В конечном итоге мне нужно преобразовать полученные формы в SVG, но я счастлив обработать преобразование в/из другой библиотеки или системы координат. То, что я ищу, - это подход к вычислению.

Предположим, что форма представляет собой четырехстороннюю форму, обрамляемую квадратично, где каждая сторона может быть вогнутой или выпуклой, но никакие края не накладываются на другие ребра или сами, а любая из четырех сторон может быть изогнута.

Тот же подход для четырехстороннего многоугольника (форма с прямыми ребрами тривиальна), а если две противоположные ребра являются прямыми линиями, легко найти пересекающиеся точки, потому что они будут лежать вдоль прямых, проведенных между подразделениями противоположных сторон, Оттуда относительно легко вычислить кривую, необходимую для присоединения к предыдущей точке вдоль альтернативной оси:

enter image description here

Однако, когда нет двух прямых, противоположных сторон (как в третьем примере выше), я не уверен, как найти точки, потому что больше нет уверенности в точках, лежащих по прямой.

Я долго искал документальный подход, но безуспешно.

Ниже приведен пример типа начальной формы с использованием SVG для его описания (он не должен обрабатываться в SVG, если я могу выводить на 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: Я задал аналогичный вопрос в Stack Exchange Maths, и один из ответов описывает один подход - использование патча Coons. Квора объясняет здесь.

Ответы

Ответ 1

Вы можете увидеть живой пример и полный код на Codepen здесь.

Представление данных

Простейшее представление данных изображения ниже использует кривые Кубического Безье. Я считаю, что это также касается всех ваших случаев использования. Чтобы не загрязнять наш код различными особыми случаями, мы будем требовать, чтобы вход всегда был в формате четырех последующих кривых Cubic Bézier, как если бы мы их рисовали. Это означает, что мы не можем использовать:

  • Кривые Quadractic Bézier (конвертируемые в Cubic путем зеркалирования другой контрольной точки)
  • Сегменты (конвертируемые в кривую Cubic Bézier путем размещения контрольных точек равноудаленно между конечными точками на линии)
  • Закрыть путь [ Z SVG command] (конвертируется в кривую Cubic Bézier, вычисляя данный сегмент, а затем беря его оттуда)

Подробнее о курсах в SVG

pure shape

Его SVG-представление

<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"
/>

Однако для удобства мы определим наши собственные структуры данных.

Point - это просто старый Vector2D класс Vector2D.

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

Curve - 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 - это всего лишь контейнер для кривых.

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 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
)

Поиск точек пересечения

intersection points

Очевидно, вы уже реализовали его с использованием подхода t (в отличие от истинной длины сращивания кривой), поэтому я помещаю его здесь только ради полноты.

Обратите внимание, что cuts - это фактическое количество точек пересечения (красных точек), которые вы получите. То есть, начальная и конечная точки не существуют (но с небольшими изменениями для cut() они могут быть)

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
  }
}

Поиск кривых расщепления

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
  }
}

Есть некоторые рыбные вещи с кодом выше.

NOTE1 Поскольку кривые представлены в том порядке, в котором вы их рисуете, противоположные стороны обращены в разные стороны. Например, верхняя сторона рисуется слева направо, а нижняя справа налево. Возможно, изображение поможет:

order of endpoints

NOTE2 Это то, как мы получаем контрольные точки для безьеров, разделяющих фигуру. t интерполяция на сегменте, соединяющем контрольные точки противоположных сторон.

Вот эти сегменты. Их конечные точки являются контрольными точками соответствующих кривых.

control point segments Inkscape screenshot

Это конечный результат при визуализации кривых: grid

Вы можете увидеть живой пример и полный код на Codepen здесь.

Куда пойти отсюда

Больше пересечений

Это, очевидно, не конечный результат. Нам еще нужно найти точки пересечения сгенерированных кривых. Однако найти пересечения двух кривых Безье нетривиально. Вот fooobar.com/questions/840839/... по теме, который приведет вас к этой аккуратной библиотеке, которая сделает тяжелую работу для вас (посмотрите на код bezier3bezier3() и вы поймете)

Разделение кривых

После того, как вы находите точки пересечения, вы хотите найти, при котором t они происходят. Почему t спросите вы? Таким образом, вы можете разделить кривую.

Фактический конечный результат

В конце вам нужно будет выбрать эти доли кривых и организовать их для представления отдельных полей сетки.

Как вы можете видеть, у вас все еще есть длинное путешествие, я только немного потрудился (и все же успел написать длинный ответ: D).

Ответ 2

Если ваши четыре стороны являются кубическими кривыми Безье, как насчет чего-то относительно простого:

Чтобы сделать горизонтальные разделители (сверху вниз), сделайте новые кубические кривые Безье, интерполируя контрольные точки верхней и нижней сторон:

Interpolating from top to bottom

Затем разделите левую и правую стороны на одинаковое количество точек.

Find points on left and right side

.. и растяните кривые делителя, чтобы они соединялись с этими точками:

enter image description here

Затем сделайте то же самое слева направо, чтобы создать вертикальные разделители.

Вот ручка для тестирования различных форм: https://codepen.io/Sphinxxxx/pen/pKddee

Важные части находятся в BezierWrapper.lerpCurve() и BezierWrapper.fitCurve(), а также класс Bezier взятый из https://gamedev.stackexchange.com/a/5427, чтобы получить равномерно разнесенные точки вдоль кривой (.samples):

class BezierWrapper {
    constructor(controls, sampleCount, classname) {
        this.controls = controls;
        this.classname = classname;

        if(sampleCount) {
            function point2obj(p) {
                return { x: p[0], y: p[1] };
            }
            //https://gamedev.stackexchange.com/a/5427
            const interpolator = new Bezier(point2obj(controls[0]),
                                            point2obj(controls[1]),
                                            point2obj(controls[2]),
                                            point2obj(controls[3]));
            const samples = this.samples = [];
            for(let i = 1; i <= sampleCount; i++) {
                const t = i / (sampleCount+1);
                samples.push([interpolator.mx(t), interpolator.my(t)]);
            }
        }
    }

    static lerpCurve(source, target, t) {

        function lerpCoord(from, to, t) {
            const diffX = to[0] - from[0],
                  diffY = to[1] - from[1],
                  lerpX = from[0] + (diffX * t),
                  lerpY = from[1] + (diffY * t);
            return [lerpX, lerpY];
        }

        const middle = source.map((c, i) => lerpCoord(c, target[i], t));
        return middle;
    }

    static fitCurve(source, start, end) {

        function distance(p1, p2) {
            const dx = p2[0] - p1[0],
                  dy = p2[1] - p1[1];
            return Math.sqrt(dx*dx + dy*dy);
        }

        //https://gist.github.com/conorbuck/2606166
        function angle(p1, p2) {
            const dx = p2[0] - p1[0],
                  dy = p2[1] - p1[1],
                  radians = Math.atan2(dy, dx);
            return radians;
        }

        //https://stackoverflow.com/info/2259476/rotating-a-point-about-another-point-2d
        function rotate(p, radians) {
            const x = p[0],
                  y = p[1],
                  cos = Math.cos(radians),
                  sin = Math.sin(radians);

            return [cos*x - sin*y, sin*x + cos*y];
        }

        const sourceStart = source[0],
              sourceEnd = source[3],
              scale = distance(start, end)/distance(sourceStart, sourceEnd),
              rot = angle(start, end) - angle(sourceStart, sourceEnd);

        //Translate, scale and rotate the source control points to make them fit the start and end points:
        const sourceNorm = source.map(c => [c[0] - sourceStart[0], c[1] - sourceStart[1]]),
              fittedNorm = sourceNorm.map(c => rotate([c[0]*scale, c[1]*scale], rot)),
              fitted = fittedNorm.map(c => [c[0] + start[0], c[1] + start[1]]);

        return fitted;
    }
}