How to judge whether a coordinate point is near a third-order Bezier curve

Recently I wrote a flowchart-related component for workflow configuration. The relationship between the nodes in it uses straight lines, polylines and curves. Because it is a flowchart, it is inevitable to operate the connecting line.

image.png

The key question is where my mouse is. I want to know whether there is a connecting line near the current mouse position, so we use the current coordinates to determine whether each connecting line is within the optional range of the connecting line.

straight line

image.png

A straight line is actually the shortest distance from a point to a line segment. This is the best solution. One judgment is enough. The following is the calculation equation:

/**
 * 求点到线段的距离
 * @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
}
复制代码

The range I judge is that if the returned value is less than 20, it is considered to be selectable.

Polyline

image.png

Judgment of a polyline and a straight line, a polyline is composed of line segments, so we judge the shortest distance from each line segment to the mouse coordinate point in turn, as long as the shortest distance we set is satisfied, the selected state of the current polyline can be returned.

// 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]
  }
}
复制代码

curve

image.png

The curves here are drawn using 3rd order Bezier .

The third-order Bessel formula:

/**
 * @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]
}
复制代码

So if we want to ask for a point on a fixed scale on the curve, we can use the above formula, for example, we want the center point:

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

Known above are the starting and ending points, two control points, and time t, and the corresponding x, y coordinates can be obtained. How to get the shortest distance from the mouse position to the curve?

Before doing this first, we need to determine the point idea of ​​​​the two control points of the curve in the process control:

GIF.gif

The red polyline is the line connecting the two start and end points of the curve and the two control points. The first control point is relative to the start point, and the second control point is relative to the end point. The offsets of the two control points are the same. , the size of the offset is calculated from the two coordinates of the starting point and the ending point:

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

Then calculate the coordinates of the control points according to the direction of the starting and ending points.

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
  }
}
复制代码
The first one: Convert the curve into a polyline, and convert the problem into the shortest distance problem from point to line segment

Because we control different t to get x, y at different positions, then the existing method can get the shortest distance from point to line segment. So we can first divide t into 100 equal parts, find the x and y of the corresponding position respectively, then combine these 100 coordinate points into 99 line segments, and finally judge the shortest distance from these 99 line segments to the mouse point in turn, as long as One of the line segments has a distance value of less than 20 from the point, and if the condition is met, it is determined that the curve is in line.

The second: Reverse the existing third-order Bessel formula to t

The idea of ​​​​this method is that Bessel's formula is based on t to find x, y, so we only need to reverse the inverse formula of using the value of x and y to find t, and then use x or y to find t, to Do a comparison of the three.

The following is the inverse t formula of the third-order Bessel formula:

/**
 * 已知四个控制点,及曲线中的某一个点的 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]
}
复制代码

According to the above inverse formula, we can use x to find the time t, and we can also use y to find the t. Each time we evaluate, we will get 3 t. According to the third-order Bezier curve, the value of t is The range is 0 - 1, so we need to determine whether t is valid after we get the value.

image.png

There are also two special kinds of curves to deal with:

  • when all points of the curve have the same x
    • When all x are the same, if we use x to find t, it will cause the Shengjin formula to not hold
  • when all points of the curve have the same y
    • When all y are the same, if we use y to find t, it will also cause Shengjin formula to not hold

So we're going to verify twice:

1. First use x to find t, then use t to find y, and then compare whether the interpolation between y and the mouse's offsetY value is within the optional range 2. If the value obtained from x is not satisfied, then use y to find t, Then use the x obtained by t to compare with the offsetX of the mouse, and return to the optional state if it is satisfied.

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]
      }
    }
  }
}
复制代码

At this point, it is completed whether the curve can be selected.

Guess you like

Origin juejin.im/post/7085635552212418574