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:
- 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$.
- 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.
- 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
}
}
}