Cómo juzgar si un punto de coordenadas está cerca de una curva Bezier de tercer orden

Recientemente escribí un componente relacionado con el diagrama de flujo para la configuración del flujo de trabajo. La relación entre los nodos utiliza líneas rectas, polilíneas y curvas. Debido a que es un diagrama de flujo, es inevitable operar la línea de conexión.

imagen.png

La pregunta clave es dónde está mi mouse. Quiero saber si hay una línea de conexión cerca de la posición actual del mouse, por lo que usamos las coordenadas actuales para determinar si cada línea de conexión está dentro del rango opcional de la línea de conexión.

línea recta

imagen.png

Una línea recta es en realidad la distancia más corta de un punto a un segmento de línea. Esta es la mejor solución. Un juicio es suficiente. La siguiente es la ecuación de cálculo:

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

El rango que juzgo es que si el valor devuelto es inferior a 20, se considera seleccionable.

Polilínea

imagen.png

Juicio de una polilínea y una línea recta, una polilínea se compone de segmentos de línea, por lo que juzgamos la distancia más corta desde cada segmento de línea hasta el punto de coordenadas del mouse, siempre que se satisfaga la distancia más corta que establecemos, el estado seleccionado de se puede devolver la polilínea actual.

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

curva

imagen.png

Las curvas aquí se dibujan usando Bézier de tercer orden.

La fórmula de Bessel de tercer orden:

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

Entonces, si queremos pedir un punto en una escala fija en la curva, podemos usar la fórmula anterior, por ejemplo, queremos el punto central:

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

Anteriormente se conocen los puntos inicial y final, dos puntos de control y el tiempo t, y se pueden obtener las correspondientes coordenadas x, y. ¿Cómo obtener la distancia más corta desde la posición del mouse hasta la curva?

Antes de hacer esto primero, necesitamos determinar la idea de punto de los dos puntos de control de la curva en el control del proceso:

GIF.gif

La polilínea roja es la línea que conecta los dos puntos inicial y final de la curva y los dos puntos de control. El primer punto de control es relativo al punto inicial y el segundo punto de control es relativo al punto final. Las compensaciones de los dos los puntos de control son los mismos. , el tamaño del desplazamiento se calcula a partir de las dos coordenadas del punto inicial y el punto final:

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

Luego calcule las coordenadas de los puntos de control de acuerdo con la dirección de los puntos inicial y final.

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
  }
}
复制代码
El primero: convertir la curva en una polilínea y convertir el problema en el problema de distancia más corta de punto a segmento de línea

Debido a que controlamos diferentes t para obtener x, y en diferentes posiciones, entonces el método existente puede obtener la distancia más corta desde el punto hasta el segmento de línea. Entonces, primero podemos dividir t en 100 partes iguales, encontrar la x y la y de la posición correspondiente respectivamente, luego combinar estos 100 puntos de coordenadas en 99 segmentos de línea y finalmente juzgar la distancia más corta desde estos 99 segmentos de línea hasta el punto del mouse. , siempre que uno de los segmentos de línea tenga un valor de distancia menor a 20 desde el punto, y si se cumple la condición, se determina que la curva está en línea.

El segundo: invertir la fórmula de Bessel de tercer orden existente para t

La idea de este método es que la fórmula de Bessel se basa en t para encontrar x, y, por lo que solo necesitamos invertir la fórmula inversa de usar el valor de x e y para encontrar t, y luego usar x o y para encontrar t, para hacer una comparación de los tres.

La siguiente es la fórmula t inversa de la fórmula de Bessel de tercer orden:

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

De acuerdo con la fórmula inversa anterior, podemos usar x para encontrar el tiempo t, y también podemos usar y para encontrar el t. Cada vez que evaluamos, obtendremos 3 t. De acuerdo con la curva de Bezier de tercer orden, el valor of t es El rango es 0 - 1, por lo que necesitamos determinar si t es válido después de obtener el valor.

imagen.png

También hay dos tipos especiales de curvas a tratar:

  • cuando todos los puntos de la curva tienen el mismo x
    • Cuando todas las x son iguales, si usamos x para encontrar t, hará que la fórmula de Shengjin no se cumpla
  • cuando todos los puntos de la curva tienen el mismo y
    • Cuando todos y son iguales, si usamos y para encontrar t, también hará que la fórmula de Shengjin no se cumpla

Así que vamos a verificar dos veces:

1. Primero use x para encontrar t, luego use t para encontrar y, y luego compare si la interpolación entre y y el valor de compensación Y del mouse está dentro del rango opcional 2. Si el valor obtenido de x no se satisface, entonces use y para encuentre t, luego use el x obtenido por t para comparar con el offsetX del mouse, y regrese al estado opcional si está satisfecho.

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

En este punto, se completa si se puede seleccionar la curva.

Supongo que te gusta

Origin juejin.im/post/7085635552212418574
Recomendado
Clasificación