Как определить, находится ли координатная точка рядом с кривой Безье третьего порядка

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

изображение.png

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

прямая линия

изображение.png

Прямая линия на самом деле является кратчайшим расстоянием от точки до отрезка. Это лучшее решение. Достаточно одного суждения. Следующее уравнение расчета:

/**
 * 求点到线段的距离
 * @param {number} pt 直线外的点
 * @param {number} p 直线内的点1
 * @param {number} q 直线内的点2
 * @returns {number} 距离
 */
function getDistance(pt: [number, number], p: [number, number], q: [number, number]) {
  const pqx = q[0] - p[0]
  const pqy = q[1] - p[1]
  let dx = pt[0] - p[0]
  let dy = pt[1] - p[1]
  const d = pqx * pqx + pqy * pqy   // qp线段长度的平方
  let t = pqx * dx + pqy * dy     // p pt向量 点积 pq 向量(p相当于A点,q相当于B点,pt相当于P点)
  if (d > 0) {  // 除数不能为0; 如果为零 t应该也为零。下面计算结果仍然成立。                   
    t /= d      // 此时t 相当于 上述推导中的 r。
  }
  if (t < 0) {  // 当t(r)< 0时,最短距离即为 pt点 和 p点(A点和P点)之间的距离。
    t = 0
  } else if (t > 1) { // 当t(r)> 1时,最短距离即为 pt点 和 q点(B点和P点)之间的距离。
    t = 1
  }

  // t = 0,计算 pt点 和 p点的距离; t = 1, 计算 pt点 和 q点 的距离; 否则计算 pt点 和 投影点 的距离。
  dx = p[0] + t * pqx - pt[0]
  dy = p[1] + t * pqy - pt[1]
  
  return dx * dx + dy * dy
}
复制代码

Диапазон, который я оцениваю, таков: если возвращаемое значение меньше 20, оно считается доступным для выбора.

Полилиния

изображение.png

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

// offsetX, offsetY 为当前鼠标位置的 x, y
for (let j = 1; j < innerPonints.length; j++) {
  pre = innerPonints[j - 1]
  cur = innerPonints[j]
  if (getDistance([offsetX, offsetY], pre, cur) < 20) {
    return points[i]
  }
}
复制代码

изгиб

изображение.png

Кривые здесь нарисованы с использованием кривых Безье 3-го порядка.

Формула Бесселя третьего порядка:

/**
 * @desc 获取三阶贝塞尔曲线的线上坐标
 * B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]
 * @param {number} t 当前百分比
 * @param {Array} p1 起点坐标
 * @param {Array} p2 终点坐标
 * @param {Array} cp1 控制点1
 * @param {Array} cp2 控制点2
 */
export const getThreeBezierPoint = (
  t: number, p1: [number, number], cp1: [number, number], 
  cp2: [number, number], p2: [number, number]
): [number, number] => {

  const [x1, y1] = p1
  const [x2, y2] = p2
  const [cx1, cy1] = cp1
  const [cx2, cy2] = cp2
  
  const x =
    x1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cx1 * t * (1 - t) * (1 - t) +
    3 * cx2 * t * t * (1 - t) +
    x2 * t * t * t
  const y =
    y1 * (1 - t) * (1 - t) * (1 - t) +
    3 * cy1 * t * (1 - t) * (1 - t) +
    3 * cy2 * t * t * (1 - t) +
    y2 * t * t * t
  return [x, y]
}
复制代码

Итак, если мы хотим запросить точку в фиксированном масштабе на кривой, мы можем использовать приведенную выше формулу, например, нам нужна центральная точка:

// 获取三阶贝塞尔曲线的中点坐标
const getBezierCenterPoint = (points: [number, number][]) => {
  return getThreeBezierPoint(
    0.5, points[0], points[1], points[2], points[3]
  )
}
复制代码

Выше известны начальная и конечная точки, две контрольные точки и время t, и можно получить соответствующие координаты x, y. Как получить кратчайшее расстояние от позиции мыши до кривой?

Прежде чем сделать это сначала, нам нужно определить точечную идею двух контрольных точек кривой в управлении процессом:

GIF.gif

Красная полилиния — это линия, соединяющая две начальную и конечную точки кривой и две контрольные точки. Первая контрольная точка относится к начальной точке, а вторая контрольная точка — к конечной точке. Смещения двух контрольные точки совпадают. , размер смещения рассчитывается из двух координат начальной точки и конечной точки:

const coeff = 0.5 // 乘积系数
// 值取起点和终点的 x、y,取两者差值的较大值 * 系数
const p = Math.max(Math.abs(destx - startx), Math.abs(desty - starty)) * coeff
复制代码

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

const coeff = 0.5
export default function calcBezierPoints({ startDire, startx, starty, destDire, destx, desty }: WF.CalcBezierType,
  points: [number, number][]) {

  const p = Math.max(Math.abs(destx - startx), Math.abs(desty - starty)) * coeff
  // 第一个控制点
  switch (startDire) {
    case 'down':
      points.push([startx, starty + p])
      break
    case 'up':
      points.push([startx, starty - p])
      break
    case 'left':
      points.push([startx - p, starty])
      break
    case 'right':
      points.push([startx + p, starty])
      break
    // no default
  }
  // 第二个控制点
  switch (destDire) {
    case 'down':
      points.push([destx, desty + p])
      break
    case 'up':
      points.push([destx, desty - p])
      break
    case 'left':
      points.push([destx - p, desty])
      break
    case 'right':
      points.push([destx + p, desty])
      break
    // no default
  }
}
复制代码
Первый: преобразовать кривую в полилинию и преобразовать задачу в задачу поиска кратчайшего расстояния от точки до отрезка.

Поскольку мы управляем разными t, чтобы получить x, y в разных позициях, существующий метод может получить кратчайшее расстояние от точки до отрезка. Таким образом, мы можем сначала разделить t на 100 равных частей, найти x и y соответствующей позиции соответственно, затем объединить эти 100 координатных точек в 99 отрезков и, наконец, определить кратчайшее расстояние от этих 99 отрезков до точки мыши по очереди. , до тех пор, пока Один из сегментов линии имеет значение расстояния менее 20 от точки, и если условие выполняется, определяется, что кривая находится на линии.

Второй: изменить существующую формулу Бесселя третьего порядка на t

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

Ниже приводится обратная формула t формулы Бесселя третьего порядка:

/**
 * 已知四个控制点,及曲线中的某一个点的 x/y,反推求 t
 * @param {number} x1 起点 x/y
 * @param {number} x2 控制点1 x/y
 * @param {number} x3 控制点2 x/y
 * @param {number} x4 终点 x/y
 * @param {number} X 曲线中的某个点 x/y
 * @returns {number[]} t[]
 */
export const getBezierT = (x1: number, x2: number, x3: number, x4: number, X: number) => {
  const a = -x1 + 3 * x2 - 3 * x3 + x4
  const b = 3 * x1 - 6 * x2 + 3 * x3
  const c = -3 * x1 + 3 * x2
  const d = x1 - X

  // 盛金公式, 预先需满足, a !== 0
  // 判别式
  const A = Math.pow(b, 2) - 3 * a * c
  const B = b * c - 9 * a * d
  const C = Math.pow(c, 2) - 3 * b * d
  const delta = Math.pow(B, 2) - 4 * A * C

  let t1 = -100, t2 = -100, t3 = -100

  // 3个相同实数根
  if (A === B && A === 0) {
    t1 = -b / (3 * a)
    t2 = -c / b
    t3 = -3 * d / c
    return [t1, t2, t3]
  }

  // 1个实数根和1对共轭复数根
  if (delta > 0) {
    const v = Math.pow(B, 2) - 4 * A * C
    const xsv = v < 0 ? -1 : 1

    const m1 = A * b + 3 * a * (-B + (v * xsv) ** (1 / 2) * xsv) / 2
    const m2 = A * b + 3 * a * (-B - (v * xsv) ** (1 / 2) * xsv) / 2

    const xs1 = m1 < 0 ? -1 : 1
    const xs2 = m2 < 0 ? -1 : 1

    t1 = (-b - (m1 * xs1) ** (1 / 3) * xs1 - (m2 * xs2) ** (1 / 3) * xs2) / (3 * a)
    // 涉及虚数,可不考虑。i ** 2 = -1
  }

  // 3个实数根
  if (delta === 0) {
    const K = B / A
    t1 = -b / a + K
    t2 = t3 = -K / 2
  }

  // 3个不相等实数根
  if (delta < 0) {
    const xsA = A < 0 ? -1 : 1
    const T = (2 * A * b - 3 * a * B) / (2 * (A * xsA) ** (3 / 2) * xsA)
    const theta = Math.acos(T)

    if (A > 0 && T < 1 && T > -1) {
      t1 = (-b - 2 * A ** (1 / 2) * Math.cos(theta / 3)) / (3 * a)
      t2 = (-b + A ** (1 / 2) * (Math.cos(theta / 3) + 3 ** (1 / 2) * Math.sin(theta / 3))) / (3 * a)
      t3 = (-b + A ** (1 / 2) * (Math.cos(theta / 3) - 3 ** (1 / 2) * Math.sin(theta / 3))) / (3 * a)
    }
  }
  return [t1, t2, t3]
}
复制代码

Согласно приведенной выше обратной формуле, мы можем использовать x, чтобы найти время t, и мы также можем использовать y, чтобы найти t. Каждый раз, когда мы оцениваем, мы получим 3 t. Согласно кривой Безье третьего порядка, значение of t is Диапазон 0–1, поэтому нам нужно определить, является ли t допустимым после того, как мы получим значение.

изображение.png

Есть также два особых типа кривых, с которыми нужно иметь дело:

  • когда все точки кривой имеют одинаковые х
    • Когда все x одинаковы, если мы используем x для нахождения t, это приведет к тому, что формула Shengjin не будет выполняться.
  • когда все точки кривой имеют одинаковые y
    • Когда все y одинаковы, если мы используем y для нахождения t, это также приведет к тому, что формула Shengjin не будет выполняться.

Итак, мы проверим дважды:

1. Сначала используйте x, чтобы найти t, затем используйте t, чтобы найти y, а затем сравните, находится ли интерполяция между y и значением смещения Y мыши в пределах необязательного диапазона 2. Если значение, полученное из x, не удовлетворяет, затем используйте y для найдите t, затем используйте x, полученный t, для сравнения со смещениемX мыши и вернитесь в необязательное состояние, если оно удовлетворено.

export const isAboveLine = (offsetX: number, offsetY: number, points: WF.LineInfo[]) => {
  // 用 x 求出对应的 t,用 t 求相应位置的 y,再比较得出的 y 与 offsetY 之间的差值
  const tsx = getBezierT(innerPonints[0][0], innerPonints[1][0], innerPonints[2][0],     innerPonints[3][0], offsetX)
  for (let x = 0; x < 3; x++) {
    if (tsx[x] <= 1 && tsx[x] >= 0) {
      const ny = getThreeBezierPoint(tsx[x], innerPonints[0], innerPonints[1], innerPonints[2], innerPonints[3])
      if (Math.abs(ny[1] - offsetY) < 8) {
        return points[i]
      }
    }
  }
  // 如果上述没有结果,则用 y 求出对应的 t,再用 t 求出对应的 x,与 offsetX 进行匹配
  const tsy = getBezierT(innerPonints[0][1], innerPonints[1][1], innerPonints[2][1], innerPonints[3][1], offsetY)
  for (let y = 0; y < 3; y++) {
    if (tsy[y] <= 1 && tsy[y] >= 0) {
      const nx = getThreeBezierPoint(tsy[y], innerPonints[0], innerPonints[1], innerPonints[2], innerPonints[3])
      if (Math.abs(nx[0] - offsetX) < 8) {
        return points[i]
      }
    }
  }
}
复制代码

На этом завершается вопрос о том, можно ли выбрать кривую.

рекомендация

отjuejin.im/post/7085635552212418574
рекомендация