Archivo de la etiqueta: topología

Detectando líneas sin continuidad

Aquí el vídeo relacionado con esta entrada

En ocasiones nos interesa realizar análisis de continuidad geométrica.

Quizás queramos detectar como error líneas que no tienen continuidad, es decir, que en las coordenadas de alguno de sus extremos no nace otra línea, o quizás justo lo contrario, una línea que continúa con otra con un código que las hace incompatibles, como curva de nivel fina con curva de nivel maestra, o dos curvas de nivel finas pero con distinta coordenada Z, …

Si queremos hacer un control de bordes, quizás nos interese detectar como errores líneas que en la práctica no tienen la obligación de continuar con otra línea, pero que finalizan en un marco de hoja y no continúan en otro archivo.

Todos estos análisis los podemos realizar mediante el tipo Digi21.DigiNG.Topology.NodeDetector que implementa una serie de métodos de extensión que se ejecutan sobre una secuencia de líneas y que nos devolverá una secuencia de IGrouping<Point2D, VertexPointer>, es decir, una secuencia de entidades agrupadas por el punto en el que coinciden esas entidades.

El tipo Digi21.DigiNG.Entities.VertexPointer es muy parecido al tipo Digi21.DigiNG.Entities.SegmentPointer, dispone de dos propiedades: una para indicar la línea que llega al nodo y otra para indicar el vértice de esa línea que llega al nodo.

A continuación la definición de este tipo:

namespace Digi21.DigiNG.Entities
{
    public struct VertexPointer
    {
        public VertexPointer(ReadOnlyLine line, int vertex);

        public ReadOnlyLine Line { get; }
        public int Vertex { get; }
    }
}

Al igual que en el caso del detector de intersecciones, dispondemos de varias sobrecargas del método de extensión que nos permitirán o detectar todos los nodos, o especificar si permitimos analizar una determinada entidad o si nos interesa un nodo en unas determinadas coordenadas o una combinación de estas opciones.

Veamos un ejemplo muy sencillo: vamos a crear una orden que añade tantas tareas en la ventana de tareas como entidades que no tienen continuidad.
Lo que vamos a hacer es detectar todos los nodos y crear una consulta Linq que se quede únicamente con la secuencia de nodos que estén formados únicamente por una entidad.

Si tenemos dos líneas, A y B, ambas formadas por dos puntos, y con las siguientes coordenadas: A: (100, 100) – (200, 200) y B: (200, 200) – (300, 100) tendremos tres nodos, uno en las coordenadas (100, 100) al que llega únicamente una entidad, la entidad A, un segundo nodo (200, 200) al que llegan dos entidades, la A y la B y por último el tercer nodo (300, 100) al que llega únicamente la entidad B.

Está claro que los nodos A y B son nodos en los que no hay continuidad pues únicamente llega a ellos una línea.

Aquí el código de la orden:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name="detectar_líneas_sin_continuidad")]
    public class DetectarLíneasSinContinuidad : Command
    {
        public DetectarLíneasSinContinuidad()
        {
            this.Initialize += new EventHandler(DetectarLíneasSinContinuidad_Initialize);
        }

        void DetectarLíneasSinContinuidad_Initialize(object sender, EventArgs e)
        {
            try
            {
                var todosLosNodos = DigiNG.DrawingFile.OfType<ReadOnlyLine>().DetectNodes();

                var nodosConUnaÚnicaLínea = from nodo in todosLosNodos
                                            where nodo.Count() == 1
                                            select nodo;

                foreach (var nodo in nodosConUnaÚnicaLínea)
                    Digi3D.Tasks.Add(new TaskEntityGotoPoint(
                        (Point3D)nodo.Key,
                        nodo.ElementAt(0).Line,
                        2,
                        "Extremo de línea sin continuidad",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_líneas_sin_continuidad"));
                DigiNG.RenderScene();
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Muy sencillo, ¿no?. Con esto hemos simulado la opción de Bintram que marca como errores entidades sin continuidad.

Vamos a añadir una tabla de códigos a nuestra orden. Ahora la orden va tener en cuenta únicamente las líneas que tengan alguno de los códigos pasados por parámetros:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using Digi21.Digi3D;
using Digi21.Math;
using UtilidadesDigi;

namespace Acme
{
    [Command(Name="detectar_líneas_sin_continuidad")]
    public class DetectarLíneasSinContinuidad : Command
    {
        public DetectarLíneasSinContinuidad()
        {
            this.Initialize += new EventHandler(DetectarLíneasSinContinuidad_Initialize);
        }

        void DetectarLíneasSinContinuidad_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_líneas_sin_continuidad",
                        "No has indicado los códigos de las entidades a analizar",
                        2,
                        BallonIcon.Error);
                    return;
                }

//                var todosLosNodos = DigiNG.DrawingFile.QueTenganAlgúnCódigo(this.Args).SoloLíneas().DetectNodes();
                var todosLosNodos = DigiNG.DrawingFile.
                    SoloLíneasSinEliminar().DetectNodes(
                    entidad => entidad.TieneAlgúnCódigo(this.Args));


                var nodosConUnaÚnicaLínea = from nodo in todosLosNodos
                                            where nodo.Count() == 1
                                            select nodo;

                foreach (var nodo in nodosConUnaÚnicaLínea)
                    Digi3D.Tasks.Add(new TaskEntityGotoPoint(
                        (Point3D)nodo.Key,
                        nodo.ElementAt(0).Line,
                        2,
                        "Extremo de línea sin continuidad",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_líneas_sin_continuidad"));
                DigiNG.RenderScene();
            }
            finally
            {
                Dispose();
            }
        }
    }
}

Detectando las intersecciones de una determinada línea

En el post anterior nos centramos en los métodos de extensión implementados en el tipo Digi21.DigiNG.Topology.IntersectionDetector que extendían secuencias de líneas.

En este post vamos a centrarnos en las sobrecargas que actúan únicamente sobre una determinada línea.

Aquí tienes el vídeo relacionado con este bloque de código

Si queremos averiguar únicamente las intersecciones que tiene una línea con el resto de líneas del modelo, podemos utilizar cualquiera de los métodos del post anterior y luego centrarnos únicamente en las intersecciones entre las que esté involucrada la línea que nos interesa.

Veámoslo con un ejemplo: Vamos a hacer una orden que solicita al usuario que se seleccione una línea y luego añadirá tantas tareas como intersecciones tenga esa línea con el resto de líneas del archivo de dibujo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Plugin.Shell;
using Digi21.Digi3D;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;

namespace Pruebas
{
    [CommandInMenu(
        "Mostrar las intersecciones de la línea seleccionada",
        MenuItemGroup.GeometricAnalysisGroup1)]
    [Command(Name="intersecciones_de_línea")]
    public class InterseccionesDeLínea : Command
    {
        public InterseccionesDeLínea()
        {
            this.Initialize += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.SetFocus += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.DataUp += new EventHandler<Digi21.Math.Point3DEventArgs>(InterseccionesDeLínea_DataUp);
            this.EntitySelected += new EventHandler<EntitySelectedEventArgs>(InterseccionesDeLínea_EntitySelected);
        }

        void InterseccionesDeLínea_SetFocus(object sender, EventArgs e)
        {
            Digi3D.StatusBar.Text = "Selecciona la línea para mostrar sus intersecciones";
        }

        void InterseccionesDeLínea_DataUp(object sender, Digi21.Math.Point3DEventArgs e)
        {
            DigiNG.SelectEntity(
                e.Coordinates,
                entidad => entidad is ReadOnlyLine);
        }

        void InterseccionesDeLínea_EntitySelected(object sender, EntitySelectedEventArgs e)
        {
            ReadOnlyLine líneaSeleccionada = (ReadOnlyLine)e.Entity;

            var todasLasIntersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections();
            foreach (var intersección in todasLasIntersecciones)
            {
                foreach (var entidadImplicadaEnLaIntersección in intersección)
                {
                    if (líneaSeleccionada == entidadImplicadaEnLaIntersección.Line)
                    {
                        Digi3D.Tasks.Add(new TaskGotoPoint(
                            (Point3D)intersección.Key,
                            "Intersección",
                            TaskSeverity.Error,
                            DigiNG.DrawingFile.Path,
                            "intersecciones_de_línea"));
                        DigiNG.RenderScene();
                        break;
                    }
                }
            }

            Dispose();
        }
    }
}

El problema que tiene este sistema es que estamos analizando las intersecciones existentes en todo el archivo de dibujo para luego centrarnos únicamente en un subconjunto en teoría muy pequeño de todas estas intersecciones de modo que estamos desaprovechando recursos (tiempo y memoria).

Afortunadamente, el tipo Digi21.DigiNG.Topology.IntersectionDetector expone métodos de extensión que afectan a una única línea. Estos métodos de extensión son idénticos a aquellos que se aplican a una secuencia de líneas, pero centrándose únicamente en las intersecciones de una determinada línea, de modo que es mucho más rápido, consume menos memoria y además ya no tenemos que buscar los nodos a los que llega nuestra línea, pues todos ellos cumplirán esta función.

La sobrecarga más sencilla es aquella que recibe como único parámetro el conjunto de líneas con las que queremos comprobar sus intersecciones.

Veamos entonces como queda nuestra orden optimizada con el método de extensión aplicado a líneas:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Plugin.Shell;
using Digi21.Digi3D;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;

namespace Pruebas
{
    [CommandInMenu(
        "Mostrar las intersecciones de la línea seleccionada",
        MenuItemGroup.GeometricAnalysisGroup1)]
    [Command(Name="intersecciones_de_línea")]
    public class InterseccionesDeLínea : Command
    {
        public InterseccionesDeLínea()
        {
            this.Initialize += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.SetFocus += new EventHandler(InterseccionesDeLínea_SetFocus);
            this.DataUp += new EventHandler<Digi21.Math.Point3DEventArgs>(InterseccionesDeLínea_DataUp);
            this.EntitySelected += new EventHandler<EntitySelectedEventArgs>(InterseccionesDeLínea_EntitySelected);
        }

        void InterseccionesDeLínea_SetFocus(object sender, EventArgs e)
        {
            Digi3D.StatusBar.Text = "Selecciona la línea para mostrar sus intersecciones";
        }

        void InterseccionesDeLínea_DataUp(object sender, Digi21.Math.Point3DEventArgs e)
        {
            DigiNG.SelectEntity(
                e.Coordinates,
                entidad => entidad is ReadOnlyLine);
        }

        void InterseccionesDeLínea_EntitySelected(object sender, EntitySelectedEventArgs e)
        {
            ReadOnlyLine líneaSeleccionada = (ReadOnlyLine)e.Entity;

            var todasLasIntersecciones =
                líneaSeleccionada.DetectIntersections(DigiNG.DrawingFile.SoloLíneas());

            foreach (var intersección in todasLasIntersecciones)
            {
                foreach (var entidadImplicadaEnLaIntersección in intersección)
                {
                    if (líneaSeleccionada == entidadImplicadaEnLaIntersección.Line)
                    {
                        Digi3D.Tasks.Add(new TaskGotoPoint(
                            (Point3D)intersección.Key,
                            "Intersección",
                            TaskSeverity.Error,
                            DigiNG.DrawingFile.Path,
                            "intersecciones_de_línea"));
                        DigiNG.RenderScene();
                        break;
                    }
                }
            }

            Dispose();
        }
    }
}

Y como este método es tan rápido, podemos permitirnos el lujo de ejecutarlo en tiempo real.

Vamos a desarrollar una orden que se ejecutará cada vez que el usuario digitaliza una línea. Si esta línea es una curva de nivel, se comprobará mediante este método si esta ha interseccionado con otra curva de nivel, en cuyo caso se mostrará al usuario una tarea, un globo y un sonido de error, con “sorpresa final”, de modo que en el mismo instante en el que el usuario digitaliza la línea, el programa ya le informa de que ha cometido un error.

Si te fijas en el código, esta orden no hace una llamada al método Dispose, por lo tanto, nunca se auto destruye. Al ejecutarla se queda residente, a la escucha, esperando que se digitalice una línea. En nomenclatura UNIX esta orden sería un demonio, en nomenclatura Windows sería una especia de proceso.
La única manera de destruir la orden es al finalizar Digi3D, el propio CLR de Windows se encargará de destruirla.

El truco para convertir esta orden en un proceso consiste en la llamada al método DigiNG.Commands.Pop(), que elimina la orden de la pila de órdenes de DigiNG, de modo que aunque el usuario pulse la tecla Escape, no puede destruir la orden, pues la tecla Escape destruye las órdenes que están en la pila de órdenes.

Y ¿cuándo entra en acción la orden?

Cada vez que DigiNG almacena una entidad en el archivo de dibujo, lanza el evento DigiNG.EntityAddes, evento al cual se puede conectar cualquier orden.

Veamos el código de la orden:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;

namespace Acme
{
    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes);

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                Digi3D.Music(MusicType.Error);
                DigiNG.RenderScene();
            }
        }


        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}

Y ahora la “sorpresa final”, ¿Por qué no hacer que en vez de sonar el sonido de error de Digi, una voz sintética hable indicándole al usuario en perfecto castellano que la curva de nivel que acaba de digitalizar se cruza X veces con otra curva de nivel?

Si hacemos que nuestro proyecto referencie el ensamblado System.Speech.dll, en el espacio de nombres System.Speech.Systhesis tenemos el tipo SpeechSynthesizer.

Lo único que tenemos que hacer es crear una instancia de este tipo y llamar a su método SpeakAsync pasando como parámetro la cadena que queremos que sea sintetizada por el motor de texto a voz.

Los sistemas operativos Windows vienen de serie con un motor de texto a voz para frases en inglés. Si quieres que el sintetizador funcione con frases en castellano tendrás que comprarte una voz en castellano. Envíame un correo si quieres que te diga dónde comprar voces en castellano.

A continuación la orden pero esta vez con voz sintética.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;
using System.Speech.Synthesis;

namespace Acme
{
    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes);

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                string mensaje = string.Format(
                    "La curva de nivel que acaba de registrar se cruza {0} veces con otra curva de nivel",
                    tareasAAñadir.Count);

                SpeechSynthesizer motorVoz = new SpeechSynthesizer();
                motorVoz.SpeakAsync(mensaje);
                DigiNG.RenderScene();
            }
        }


        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}

Aquí tienes el vídeo de esta sección de código

y para terminar, vamos a hacer que la orden no detecte como erróneas intersecciones en las que una curva de nivel continúa a otra curva de nivel (es decir, si el nodo de intersección coincide con el comienzo o final de ambas curvar) y además vamos a solucionar un problema gramatical para evitar que la voz sintética diga “… se ha cruzado una veces con…”

Para comprobar si una intersección se ha realizado al comienzo o al final de una determinada línea, vamos a crear un método de extensión para el tipo SegmentPointer que nos devolverá verdadero si se cumple esta condición, facilitando mucho la lectura del código principal.

Aquí tienes el código definitivo:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.DigiNG.Topology;
using Digi21.Math;
using System.Speech.Synthesis;

namespace Acme
{
    public static class UtilidadesSegmentPointer
    {
        public static bool IntersecciónAlComienzoOFinalDeLínea(this SegmentPointer entidad)
        {
            if (entidad.FirstVertex == entidad.SecondVertex &&
                (entidad.FirstVertex == 0 ||
                entidad.FirstVertex == entidad.Line.Points.Count - 1))
                return true;

            return false;
        }
    }

    [Command(Name="servicio_curvas_nivel")]
    public class ServicioCurvasNivel : Command
    {
        public ServicioCurvasNivel()
        {
            this.Initialize += new EventHandler(ServicioCurvasNivel_Initialize);
            DigiNG.EntityAdded += new EventHandler<EntityAddedEventArgs>(DigiNG_EntityAdded);
        }

        void DigiNG_EntityAdded(object sender, EntityAddedEventArgs e)
        {
            if (e.Entity is ReadOnlyLine &&
                e.Entity.TieneAlgúnCódigo(this.Args))
                ControlCalidadCurvasNivel(e.Entity as ReadOnlyLine);
        }

        private void ControlCalidadCurvasNivel(ReadOnlyLine línea)
        {
            var curvasDeNivelExistentes = from entidad in DigiNG.DrawingFile
                                          where entidad != línea
                                          where entidad.TieneAlgúnCódigo(this.Args)
                                          select entidad as ReadOnlyLine;

            var intersecciones = línea.DetectIntersections(curvasDeNivelExistentes).ToArray();

            if (intersecciones.Length == 1)
            {
                if (intersecciones[0].Count() == 2)
                {
                    SegmentPointer líneaA = intersecciones[0].ElementAt(0);
                    SegmentPointer líneaB = intersecciones[0].ElementAt(1);

                    if (líneaA.IntersecciónAlComienzoOFinalDeLínea() &&
                        líneaB.IntersecciónAlComienzoOFinalDeLínea())
                        return;
                }
            }

            List<ITask> tareasAAñadir = new List<ITask>();
            foreach (var intersección in intersecciones)
            {
                tareasAAñadir.Add(new TaskEntityGotoPoint(
                    (Point3D)intersección.Key,
                    línea,
                    2,
                    "Curva de nivel que cruza con otras curvas de nivel",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "servicio_curvas_nivel"));
            }

            foreach (var tarea in tareasAAñadir)
                Digi3D.Tasks.Add(tarea);

            if (tareasAAñadir.Count != 0)
            {
                string mensaje;

                if( tareasAAñadir.Count == 1 )
                    mensaje = "La curva de nivel que acaba de registrar se cruza una vez con otra curva de nivel";
                else
                    mensaje = string.Format(
                        "La curva de nivel que acaba de registrar se cruza {0} veces con otra curva de nivel",
                        tareasAAñadir.Count);

                SpeechSynthesizer motorVoz = new SpeechSynthesizer();
                motorVoz.SpeakAsync(mensaje);
                DigiNG.RenderScene();
            }
        }


        void ServicioCurvasNivel_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 0)
            {
                Digi3D.Music(MusicType.Error);
                Digi3D.ShowBallon(
                    "servicio_curvas_nivel",
                    "No se han indicado los códigos de las curvas de nivel",
                    2,
                    BallonIcon.Error);
                Dispose();
                return;
            }

            DigiNG.Commands.Pop();
        }
    }
}

Detectando intersecciones entre múltiples líneas

Podemos detectar intersecciones entre líneas mediante el tipo Digi21.DigiNG.Topology.IntersectionDetector, que es una clase abstracta que define métodos de extensión sobre el tipo Digi21.DigiNG.ReadOnlyLine o sobre una secuencia de líneas IEnumerable<Digi21.DigiNG.ReadOnlyLine>.

Estos métodos de extensión disponen de varias sobrecargas que nos van a permitir filtrar el tipo de intersecciones que queremos detectar, de manera que podemos llegar a realizar consultas con un altísimo nivel de detalle, por ejemplo, podríamos realizar búsquedas tan extrañas como intersecciones entre dos líneas, la primera forzosamente con el código A y un número par de vértices y la segunda con código B y con un número par de vértices.
Las posibilidades son infinitas.

Todas las sobrecargas devuelven una secuencia objetos de Digi21.DigiNG.Entities.SegmentPointer como por ejemplo el siguiente recorte en el que te muestro la primera sobrecarga:

        public static IEnumerable<IGrouping<Point2D, SegmentPointer>> DetectIntersections(
            this IEnumerable<ReadOnlyLine> entities);

Y por su parte, el tipo SegmentPointer tiene el siguiente aspecto:

        public class SegmentPointer
        {
            public SegmentPointer(
                ReadOnlyLine line,
                int firstVertex,
                int secondVertex);

            public int FirstVertex { get; }
            public ReadOnlyLine Line { get; }
            public int SecondVertex { get; }
        }

que si te fijas tiene tres propiedades: Una que devuelve la línea implicada en la intersección, y otras dos que nos indican entre que vértices de esa línea se ha detectado la intersección. Si el valor almacenado en estas dos últimas propiedades coincide, significa que la intersección se detectó en ese vértice, y si no coinciden, las coordenadas de la intersección estarán en el segmento que une los dos vértices.

Utilizar estos métodos es trivial, tan solo tenemos que añadir una referencia al ensamblado Digi21.DigiNG.Topology, hacer visible en nuestro código el espacio de nombres Digi21.DigiNG.Topology y llamar al método de extensión que nos interese o sobre una línea o sobre una secuencia de líneas.

En este post nos vamos a centrar únicamente en los métodos de extensión que se aplican a secuencias de entidades. En el próximo post nos centraremos en las sobrecargas que se aplican a una línea en concreto.

Aquí el vídeo relacionado con este código

El siguiente código detecta las intersecciones de todas las entidades del archivo de dibujo activo, añadiendo una tarea en la ventana de tareas de la aplicación por cada intersección, de modo que cuando el usuario hace doble clic sobre la tarea se desplaza la ventana de dibujo a las coordenadas de la intersección:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.DigiNG.Topology;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using Digi21.Math;

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            foreach (var intersección in líneas.DetectIntersections())
            {
                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    "Detectada una intersección",
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones"));
            }

            Dispose();
        }
    }
}

Pero podemos hacer las cosas mejor, ya que el método lo que nos ha devuelto es una secuencia de grupos (cuya clave principal es la coordenada de la intersección), así que podríamos hacer que en la barra de tareas apareciera únicamente una tarea por cada punto en el que se ha detectado una intersección y que dicha tarea tenga como nodos hijos una subtarea por cada entidad. Al hacer doble clic sobre la tarea se desplazará la cámara a las coordenadas de la intersección, y al hacer doble clic sobre cada subtarea, se animará durante dos segundos la línea en cuestión:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.DigiNG.Topology;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using Digi21.Math;

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            foreach (var intersección in líneas.DetectIntersections())
            {
                List<ITask> subtareas = new List<ITask>();
                foreach (var entidad in intersección)
                {
                    string títuloSubtarea = string.Format(
                        "Intersección entre los vértices {0} y {1}",
                        entidad.FirstVertex,
                        entidad.SecondVertex);

                    subtareas.Add(new TaskAnimateEntity(
                        entidad.Line,
                        2,
                        títuloSubtarea,
                        TaskSeverity.Error));
                }

                string títuloTarea = string.Format(
                    "Detectada una intersección en las coordenadas {0}",
                    intersección.Key);

                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    títuloTarea,
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones",
                    subtareas.ToArray()));
            }

            Dispose();
        }
    }
}

Especializaciones

Los métodos de extensión DetectIntersections disponen de varias sobrecargas que nos van a permitir realizar búsquedas especializadas.

Aquí el vídeo relacionado con este código

La primera de ellas nos permite que pasemos un predicado de tipo Func<Point2D, bool> para indicar si queremos que se añada una determinada intersección al resultado.

Para ello vamos a modificar nuestra orden para hacer que únicamente detecte intersecciones dentro de un rango de coordenadas máximas y mínimas pasadas por parámetro.
Si el usuario no pasa parámetros a la orden, se mostrarán todas las intersecciones, y si pasa cuatro parámetros (xmin, ymin, xmax, ymax) se mostrarán únicamente las intersecciones dentro de este rango de coordenadas.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG;
using Digi21.DigiNG.Topology;
using Digi21.Digi3D;
using Digi21.DigiNG.Entities;
using Digi21.Math;

namespace Pruebas
{
    [Command(Name = "detectar_intersecciones")]
    public class DetectarIntersecciones : Command
    {
        private Window2D maxmin = Window2D.WholeWorld;

        public DetectarIntersecciones()
        {
            this.Initialize += new EventHandler(DetectarIntersecciones_Initialize);
        }

        void DetectarIntersecciones_Initialize(object sender, EventArgs e)
        {
            if (this.Args.Length == 4)
            {
                maxmin.Xmin = double.Parse(this.Args[0]);
                maxmin.Ymin = double.Parse(this.Args[1]);
                maxmin.Xmax = double.Parse(this.Args[2]);
                maxmin.Ymax = double.Parse(this.Args[3]);
            }

            var líneas = from entidad in DigiNG.DrawingFile
                         where entidad is ReadOnlyLine
                         select entidad as ReadOnlyLine;

            var intersecciones = líneas.DetectIntersections(
                (Point2D coordenada) => maxmin.Contains(coordenada));

            foreach (var intersección in intersecciones)
            {
                List<ITask> subtareas = new List<ITask>();
                foreach (var entidad in intersección)
                {
                    string títuloSubtarea = string.Format(
                        "Intersección entre los vértices {0} y {1}",
                        entidad.FirstVertex,
                        entidad.SecondVertex);

                    subtareas.Add(new TaskAnimateEntity(
                        entidad.Line,
                        2,
                        títuloSubtarea,
                        TaskSeverity.Error));
                }

                string títuloTarea = string.Format(
                    "Detectada una intersección en las coordenadas {0}",
                    intersección.Key);

                Digi3D.Tasks.Add(new TaskGotoPoint(
                    (Point3D)intersección.Key,
                    títuloTarea,
                    TaskSeverity.Error,
                    DigiNG.DrawingFile.Path,
                    "detectar_intersecciones",
                    subtareas.ToArray()));
            }

            Dispose();
        }
    }
}

Pero hay más sobrecargas: La siguiente nos permite indicar si queremos o no incluir una línea (mediante un predicado de tipo Func<Entity, bool>) en el proceso.

Podemos utilizar esta sobrecarga para filtrar por códigos por ejemplo.

Aquí el vídeo relacionado con este código
Veámoslo creando una nueva orden que detecta intersecciones entre las líneas con los códigos pasados por parámetro.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_cod")]
    public class DetectarInterseccionesCod : Command
    {
        public DetectarInterseccionesCod()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCod_Initialize);
        }

        //bool MeInteresaEstaLínea(ReadOnlyLine línea)
        //{
        //    return línea.TieneAlgúnCódigo(this.Args);
        //}

        void DetectarInterseccionesCod_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length == 0)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_cod",
                        "No has indicado los códigos de las entidades para la detección de intersecciones",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    línea => línea.TieneAlgúnCódigo(this.Args));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}

La siguiente sobrecarga nos permite especificar si permitimos analizar las intersecciones entre dos líneas. Cada vez que el algoritmo detecta que dos líneas son candidatas a tener intersecciones, el algoritmo llama a nuestro predicado para que le indiquemos si le damos permiso para analizar las intersecciones entre esas dos líneas.

Supón que quieres que únicamente se analicen las intersecciones entre línas de tipo A con líneas de tipo B, pero nunca intersecciones de líneas de tipo A con líneas de tipo A y de tipo B con tipo B

Aquí el vídeo relacionado con este código

Vamos a crear una orden nueva que esperae dos códigos y analice únicamente las intersecciones entre líneas que tengan una el primer código y otra el segundo o viceversa, pero nunca intersecciones de dos líneas con el mismo código.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_códigos_distintos")]
    public class DetectarInterseccionesCódigosDistintos : Command
    {
        public DetectarInterseccionesCódigosDistintos()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCódigosDistintos_Initialize);
        }

        //private bool PermitoCalcularInterseccionesEntreEstasDosLíneas(ReadOnlyLine líneaA, ReadOnlyLine líneaB)
        //{
        //    return líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
        //        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]);
        //}

        void DetectarInterseccionesCódigosDistintos_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length != 2)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_códigos_distintos",
                        "No has indicado los dos códigos con los que trabajar",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    (líneaA, líneaB) =>
                        líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
                        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}

Pero podríamos haber optimizado el proceso anterior, ya que el algoritmo va a llamar a nuestro predicado por todas las entidades que tengan posibilidad de cruzarse, independientemente de sus códigos.

El resto de sobrecargas son combinaciones las sobrecargas anteriores, de modo que existe una que nos permite pasarle dos predicados: uno para indicar si analizar las intersecciones de una línea y otro para indicar si permitimos analizar las intersecciones entre un par de líneas, de modo que la siguiente modificación mejora sustancialmente la velocidad de proceso del algoritmo con idéntico resultado:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Digi21.DigiNG.Plugin.Command;
using Digi21.DigiNG.Topology;
using Digi21.DigiNG;
using Digi21.DigiNG.Entities;
using UtilidadesDigi;
using Digi21.Digi3D;
using Digi21.Math;

namespace Acme
{
    [Command(Name = "detectar_intersecciones_códigos_distintos")]
    public class DetectarInterseccionesCódigosDistintos : Command
    {
        public DetectarInterseccionesCódigosDistintos()
        {
            this.Initialize += new EventHandler(DetectarInterseccionesCódigosDistintos_Initialize);
        }

        //private bool PermitoCalcularInterseccionesEntreEstasDosLíneas(ReadOnlyLine líneaA, ReadOnlyLine líneaB)
        //{
        //    return líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
        //        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]);
        //}

        void DetectarInterseccionesCódigosDistintos_Initialize(object sender, EventArgs e)
        {
            try
            {
                if (this.Args.Length != 2)
                {
                    Digi3D.Music(MusicType.Error);
                    Digi3D.ShowBallon(
                        "detectar_intersecciones_códigos_distintos",
                        "No has indicado los dos códigos con los que trabajar",
                        2,
                        BallonIcon.Error);
                    return;
                }

                var intersecciones = DigiNG.DrawingFile.SoloLíneas().DetectIntersections(
                    línea => línea.TieneAlgúnCódigo(this.Args),
                    (líneaA, líneaB) =>
                        líneaA.TieneElCódigo(this.Args[0]) && líneaB.TieneElCódigo(this.Args[1]) ||
                        líneaA.TieneElCódigo(this.Args[1]) && líneaB.TieneElCódigo(this.Args[0]));

                foreach (var intersección in intersecciones)
                {
                    List<ITask> subtareas = new List<ITask>();
                    foreach (var entidad in intersección)
                    {
                        string títuloSubtarea = string.Format(
                            "Intersección entre los vértices {0} y {1}",
                            entidad.FirstVertex,
                            entidad.SecondVertex);

                        subtareas.Add(new TaskAnimateEntity(
                            entidad.Line,
                            2,
                            títuloSubtarea,
                            TaskSeverity.Error));
                    }

                    Digi3D.Tasks.Add(new TaskGotoPoint(
                        (Point3D)intersección.Key,
                        "Detectada una intersección",
                        TaskSeverity.Error,
                        DigiNG.DrawingFile.Path,
                        "detectar_intersecciones",
                        subtareas.ToArray()));
                }
                DigiNG.RenderScene();

            }
            finally
            {
                Dispose();
            }
        }
    }
}