Determinación de la Posición de un Punto Respecto a un Segmento Usando Producto Escalar

La determinación de si un punto se encuentra dentro, fuera o en los extremos de un segmento de línea puede realizarse analizando los ángulos formados entre los vectores. Mientras que el producto cruz es útil para determinar si un punto está a la izquierda o derecha de una línea, el producto escalar es más adecuado para distinguir entre ángulos agudos y obtusos, lo cual es clave para determinar la posición externa de un punto respecto a un segmento.

Principios de Análisis por Producto Escalar

El porducto escalar de dos vectores, $\vec{u} \cdot \vec{v} = |\vec{u}| |\vec{v}| \cos(\theta)$, nos da información sobre el ángulo $\theta$ entre ellos:

  • Si $\cos(\theta) > 0$, el ángulo es agudo ($0^\circ \le \theta < 90^\circ$).
  • Si $\cos(\theta) < 0$, el ángulo es obtuso ($90^\circ < \theta \le 180^\circ$).
  • Si $\cos(\theta) = 0$, el ángulo es recto ($ \theta = 90^\circ$).

Consideremos un punto $P$ y un segmeento definido por los puntos $A$ y $B$. Formamos los vectores $\vec{AP} = P - A$ y $\vec{AB} = B - A$.

Casos de Posición del Punto P:

  1. Punto Exterior al Segmento:
  • El ángulo entre $\vec{AP}$ y $\vec{AB}$ es obtuso o de $180^\circ$. Esto significa que el producto escalar $\vec{AP} \cdot \vec{AB} < 0$.
  • Alternativamente, si consideramos el vector $\vec{BA} = A - B$, el ángulo entre $\vec{BP} = P - B$ y $\vec{BA}$ es obtuso o de $180^\circ$. Esto implica $\vec{BP} \cdot \vec{BA} < 0$.
  1. Punto Coincidente con un Extremo del Segmento:
  • Si $P$ coincide con $A$, la magnitud del vector $\vec{AP}$ es cero.
  • Si $P$ coincide con $B$, la magnitud del vector $\vec{BP}$ es cero.
  1. Punto Interior al Segmento:
  • Si el punto $P$ se encuentra entre $A$ y $B$, entonces los vectores $\vec{AP}$ y $\vec{AB}$ son colineales y apuntan en la misma dirección (ángulo de $0^\circ$), o bien, $\vec{BP}$ y $\vec{BA}$ son colineales y apuntan en la misma dirección. En estos casos, el producto escalar será positivo.
  • Los ángulos formados en los extremos $A$ y $B$ con el punto $P$ son agudos.

Implementación en Código (C# con Unity)

using UnityEngine;

public static class SegmentGeometry
{
    /// <summary>
    /// Determina si un punto está fuera de los límites de un segmento.
    /// </summary>
    /// <param name="point">El punto a verificar.</param>
    /// <param name="segmentStart">El punto inicial del segmento.</param>
    /// <param name="segmentEnd">El punto final del segmento.</param>
    /// <param name="isNearStartPoint">Indica si el punto está en la zona exterior cercana al punto inicial del segmento.</param>
    /// <returns>True si el punto está fuera del segmento, False en caso contrario.</returns>
    public static bool IsPointOutsideSegment(Vector2 point, Vector2 segmentStart, Vector2 segmentEnd, out bool isNearStartPoint)
    {
        isNearStartPoint = false;

        Vector2 vecAP = point - segmentStart;
        Vector2 vecAB = segmentEnd - segmentStart;

        // Si el producto escalar es negativo, el ángulo AP-AB es obtuso o 180 grados.
        // Esto significa que el punto está en la zona exterior de A.
        if (Vector2.Dot(vecAP, vecAB) < 0f)
        {
            isNearStartPoint = true;
            return true;
        }

        Vector2 vecBA = -vecAB; // Vector BA es el opuesto a AB
        Vector2 vecBP = point - segmentEnd;

        // Si el producto escalar vecBP . vecBA es negativo, el ángulo BP-BA es obtuso o 180 grados.
        // Esto significa que el punto está en la zona exterior de B.
        if (Vector2.Dot(vecBP, vecBA) < 0f)
        {
            return true;
        }

        // Si no se cumplen las condiciones anteriores, el punto está dentro o en los extremos del segmento.
        return false;
    }

    /// <summary>
    /// Clasifica la posición de un punto respecto a un segmento.
    /// </summary>
    /// <param name="point">El punto a clasificar.</param>
    /// <param name="segmentStart">El punto inicial del segmento.</param>
    /// <param name="segmentEnd">El punto final del segmento.</param>
    /// <param name="isNearStart">Indica si el punto está en la zona exterior cercana al punto inicial o si coincide con él.</param>
    /// <returns>
    /// -1: El punto está en el exterior del segmento.
    ///  0: El punto coincide con uno de los extremos del segmento.
    ///  1: El punto está en el interior del segmento.
    /// </returns>
    public static int ClassifyPointToSegment(Vector2 point, Vector2 segmentStart, Vector2 segmentEnd, out bool isNearStart)
    {
        isNearStart = false;

        Vector2 vecAP = point - segmentStart;
        Vector2 vecAB = segmentEnd - segmentStart;

        // Comprobación de exterior cerca de A
        if (Vector2.Dot(vecAP, vecAB) < 0f)
        {
            isNearStart = true;
            return -1; // Exterior
        }

        Vector2 vecBA = -vecAB;
        Vector2 vecBP = point - segmentEnd;

        // Comprobación de exterior cerca de B
        if (Vector2.Dot(vecBP, vecBA) < 0f)
        {
            return -1; // Exterior
        }

        // Comprobación de coincidencia con extremos
        // Usamos sqrMagnitude para evitar la raíz cuadrada y comparar con un pequeño epsilon.
        float squaredDistanceAP = vecAP.sqrMagnitude;
        if (squaredDistanceAP < Mathf.Epsilon) // Mathf.Epsilon es un valor muy pequeño
        {
            isNearStart = true;
            return 0; // Coincide con A
        }

        float squaredDistanceBP = vecBP.sqrMagnitude;
        if (squaredDistanceBP < Mathf.Epsilon)
        {
            return 0; // Coincide con B
        }

        // Si no es exterior ni coincide con los extremos, entonces está en el interior.
        return 1; // Interior
    }
}

Ejemplo de Uso y Visualización (Unity)

Este script de prueba utiliza los métodos anteriores y visualiza los resultados en el editor de Unity.

using System;
using UnityEngine;

public class PointSegmentRelationTest : MonoBehaviour
{
    public Transform pointATransform; // Transform para el punto A del segmento
    public Transform pointBTransform; // Transform para el punto B del segmento
    public Transform pointPTransform; // Transform para el punto P a evaluar

    public int apiSelection = 1; // 1: IsPointOutsideSegment, 2: ClassifyPointToSegment
    public int invokeCount = 100; // Número de veces que se llama a la función para pruebas de rendimiento

    private bool isPointOutside;
    private bool isNearA;
    private int pointClassification;

    void Update()
    {
        if (pointATransform != null && pointBTransform != null && pointPTransform != null)
        {
            Vector2 pointA = pointATransform.position;
            Vector2 pointB = pointBTransform.position;
            Vector2 pointP = pointPTransform.position;

            var startTime = DateTime.Now;

            switch (apiSelection)
            {
                case 1:
                    for (int i = 0; i < invokeCount; i++)
                    {
                        isPointOutside = SegmentGeometry.IsPointOutsideSegment(pointP, pointA, pointB, out isNearA);
                    }
                    break;
                case 2:
                    for (int i = 0; i < invokeCount; i++)
                    {
                        pointClassification = SegmentGeometry.ClassifyPointToSegment(pointP, pointA, pointB, out isNearA);
                    }
                    break;
            }
            // Aquí se podría añadir lógica para medir el tiempo de ejecución si fuera necesario.
        }
    }

    private void OnDrawGizmos()
    {
        if (pointATransform != null && pointBTransform != null && pointPTransform != null)
        {
            Vector2 pointA = pointATransform.position;
            Vector2 pointB = pointBTransform.position;

            Gizmos.color = Color.gray; // Dibuja el segmento base
            Gizmos.DrawLine(pointA, pointB);

            // Dibuja la línea según el resultado de la prueba
            if (apiSelection == 1)
            {
                if (isPointOutside)
                {
                    Gizmos.color = isNearA ? Color.red : Color.blue; // Rojo si está cerca de A, Azul si cerca de B
                    Gizmos.DrawLine(pointA, pointB);
                }
            }
            else if (apiSelection == 2)
            {
                if (pointClassification == -1) // Exterior
                {
                    Gizmos.color = isNearA ? Color.red : Color.blue; // Rojo si está cerca de A, Azul si cerca de B
                    Gizmos.DrawLine(pointA, pointB);
                }
                else if (pointClassification == 0) // Coincidente
                {
                    Gizmos.color = Color.yellow; // Amarillo si coincide con un extremo
                    Gizmos.DrawSphere(isNearA ? pointA : pointB, 0.1f);
                }
            }

            // Dibujar vectores normales para indicar las direcciones "fuera"
            Vector2 segmentVector = pointB - pointA;
            Vector2 normalLeft = new Vector2(-segmentVector.y, segmentVector.x).normalized;
            Vector2 normalRight = new Vector2(segmentVector.y, -segmentVector.x).normalized;

            Gizmos.color = new Color(0.5f, 0f, 1f, 0.5f); // Morado semitransparente
            Gizmos.DrawLine(pointA, pointA + normalLeft * 1.0f);
            Gizmos.DrawLine(pointA, pointA + normalRight * 1.0f);
            Gizmos.DrawLine(pointB, pointB + normalLeft * 1.0f);
            Gizmos.DrawLine(pointB, pointB + normalRight * 1.0f);

            Gizmos.color = Color.white; // Restaura el color por defecto
        }
    }
}

Etiquetas: geometría computacional producto escalar vector2 Unity C#

Publicado el 6-10 07:33