ONCE.12. MANIPULACIÓN DE CADENAS DE TEXTO

Explicaremos a continuación todo lo referente a las funciones de AutoLISP para el manejo de cadenas de texto. Es frecuente en un programa la aparición de mensajes en la línea de comandos, para la solicitud de datos por ejemplo. Pues bien, muchas veces nos interesará utilizar las funciones que aprenderemos a continuación para que dichos mensajes sean más interesantes o prácticos. Además, determinadas especificaciones de un dibujo en la Base de Datos de AutoCAD se encuentran almacenadas como cadenas de texto, léase nombres de capa, estilos de texto, variables de sistema, etcétera. Por todo ello, será muy interesante asimilar bien los conocimientos sobre cadenas de texto para ascender un escalafón más en la programación en AutoLISP para AutoCAD 14.

Comencemos pues, sin más dilación, con una función sencilla:

(STRCASE cadena [opción])

STRCASE toma la cadena de texto especificada en cadena y la convierte a mayúsculas o minúsculas según opción. Al final se devuelve el resultado de la conversión.

Si opción no existe o es nil, la cadena se convierte a mayúsculas. Si opción es T, la cadena de convierte a minúsculas. Veamos unos ejemplos:

(STRCASE "Esto es un ejemplo") devuelve "ESTO ES UN EJEMPLO"
(STRCASE "Esto es un ejemplo" nil)
devuelve "ESTO ES UN EJEMPLO"
(STRCASE "Esto es un ejemplo" T)
devuelve "esto es un ejemplo"
(STRCASE "Esto es un ejemplo" (= 3 3))
devuelve "esto es un ejemplo"
(STRCASE "Esto es un ejemplo" (/= 3 3))
devuelve "ESTO ES UN EJEMPLO"
(STRCASE "MINÚSCULAS" T)
devuelve "minúsculas"
(STRCASE "mayúsculas")
devuelve "MAYÚSCULAS"

 

La siguiente función es muy usada a la hora de programar, como veremos. STRCAT, que así se llama, devuelve una cadena que es la suma o concatenación de todas las cadenas especificadas. Veamos su sintaxis:

(STRCAT cadena1 [cadena2...])

Un ejemplo puede ser el siguiente:

(SETQ cad1 "Esto es un ")
(SETQ cad2 "ejemplo de")
(SETQ cad3 " concatenación ")
(SETQ cad4 "de cadenas.")
(STRCAT cad1 cad2 cad3)

Esto devuelve lo siguiente:

"Esto es un ejemplo de concatenación de cadenas."

Como vemos, ya sea en un lado o en otro, hemos de dejar los espacios blancos convenientes para que la oración sea legible. Un espacio es un carácter ASCII más, por lo que se trata igual que los demás.

Los argumentos de STRCAT han de ser cadenas forzosamente, de lo contrario AutoLISP mostrará un mensaje de error.

NOTA: Recordamos que al final de este MÓDULO existe una sección en la que se muestran todos los mensajes de error de AutoLISP con sus significados correspondientes.

Cada cadena únicamente puede contener 132 caracteres, sin embargo es posible concatenar varios textos hasta formar cadenas más largas.

Una utilidad muy interesante de esta función es la de visualizar mensajes que dependen del contenido de ciertas variables, por ejemplo:

(SETQ NombreBloque (GETSTRING "Nombre del bloque: "))
(SETQ PuntoIns (GETPOINT (STRCAT "Punto de inserción del
bloque " NombreBloque ": "))

Y también con variables de tipo numérico, que deberemos convertir antes en un cadena con alguna de las funciones aprendidas en la sección anterior:

(SETQ Var1 (GETINT "Radio del círculo base: "))
(SETQ Var2 (GETINT (STRCAT "Número de círculos de radio " (ITOA Var1)
" que se dibujarán en una línea")))

De esta manera, pensemos que podemos introducir, en esa pequeña cuña que es la variable dentro del texto, el último dato introducido por el usuario como valor por defecto, por ejemplo. Lo veremos en algún ejemplo o ejercicio a lo largo de este MÓDULO.

(SUBSTR cadena posición [longitud...])

Esta función extrae longitud caracteres de cadena desde posición inclusive. Esto es, devuelve una subcadena, que extrae de la cadena principal, a partir de la posición indicada y hacia la derecha, y que tendrá tantos caracteres de longitud como se indique.

Tanto la posición de inicio como la longitud han de ser valores enteros y positivos. Veamos unos ejemplos:

(SETQ Cadena "Buenos días")

(SUBSTR Cadena 2 3) devuelve "uen"
(SUBSTR Cadena 1 7)
devuelve "Buenos "
(SUBSTR Cadena 7 1)
devuelve " "
(SUBSTR Cadena 11 1)
devuelve "s"
(SUBSTR Cadena 11 17)
devuelve "s"
(SUBSTR Cadena 1 77)
devuelve "Buenos días"

(STRLEN [cadena1 cadena2...])

STRLEN devuelve la longitud de la cadena indicada. Si no se indica ninguna o se indica una cadena vacía (""), STRLEN devuelve 0. El valor de la longitud es un número entero que expresa el total de caracteres de la cadena. Si se indican varias cadenas devuelve la suma total de caracteres. Ejemplos:

(STRLEN "Buenos días") devuelve 11
(STRLEN "Hola" "Buenos días")
devuelve 15
(STRLEN)
devuelve 0

(SETQ C1 "Hola, " C2 "buenos días.")
(STRLEN (STRCAT C1 C2))
devuelve 18

 

(ASCII cadena)

ASCII devuelve un valor entero que es el código decimal ASCII del primer carácter de la cadena indicada. Veamos unos ejemplos:

(ASCII "d") devuelve 100
(ASCII "7")
devuelve 55
(ASCII "+")
devuelve 43
(ASCII "AutoLISP")
devuelve 65
(ASCII "Programación")
devuelve 80

Esta función puede ser interesante a la hora de capturar pulsaciones de teclas. Veamos el siguiente ejemplo:

(SETQ Tecla (GETSTRING "Teclee un radio o INTRO para terminar: "))
--(WHILE (/= (ASCII Tecla) 0)
--(PROMPT "Aún no terminamos...")
--(SETQ Tecla (GETSTRING "\nTeclee un radio o INTRO para terminar: "))
)
(PROMPT "FIN.")

En el momento en que pulsemos INTRO, Tecla guardará una respuesta nula cuyo código ASCII es 0. En ese momento el programa acabará. No confundir con el código ASCII del INTRO que es el 13, que no podríamos utilizar porque lo que se guarda en Tecla —que es lo que se compara— al pulsar INTRO es una cadena vacía "".

(CHR código_ASCII)

CHR funciona complementariamente a ASCII, es decir, devuelve el carácter cuyo código ASCII coincide con el valor especificado. Ejemplos:

(CHR 54) devuelve "6"
(CHR 104)
devuelve "h"
(CHR 0)
devuelve ""

NOTA: Apréciese que CHR devuelve cadenas de texto entrecomilladas.

(WCMATCH cadena filtro)

Esta función aplica un filtro o patrón a la cadena de texto. Se compara pues la cadena con dicho patrón indicado y se devuelve T si lo cumple; si no se devuelve nil.

La manera de formar filtros es mediante un conjunto de caracteres globales o comodín, que algunos recuerdan a la forma de trabajo al más puro estilo MS-DOS. La relación y significado de los posibles filtros utilizables se muestra en la siguiente tabla:

Carácter ---- Nombre ----------- Definición

--------------------------------------------------------------------------------

# ------------ Almohadilla -------- Cualquier dígito numérico.

@ ------------ A de arroba ------- Cualquier carácter alfabético.

. ------------ Punto ------------- Cualquier carácter no alfanumérico.

* ------------ Asterisco --------- Cualquier secuencia de caracteres, incluida una vacía.

? ------------ Signo de interrogación - Cualquier carácter.

~ ------------ Tilde (ALT+126) --- Si es el primer carácter del patrón, cualquier elemento excepto el patrón.

[...] ------ Corchetes quebrados - Cualquiera de los caracteres encerrados.

[~...] ------ ~ + [] ------------- Cualquiera de los caracteres no encerrados.

- ------------ Guión ------------ Entre corchetes siempre para especificar un rango para
----------------------------------- un carácter único.

, ------------ Coma -------------- Separa dos patrones.

‘------------ Apóstrofo invertido -- Omite caracteres especiales (lee el siguiente carácter de forma literal).

Nota: Si la cadena es muy larga se comparan sólo del orden de 500 caracteres.

Veamos una serie de ejemplos:

— Detectar si una cadena comienza con la letra "B":

(WCMATCH "Bloques" "B*") devuelve T

— Detectar si una cadena tiene cinco caracteres:

(WCMATCH "Bloques" "?????") devuelve nil

— Detectar si una cadena contiene la letra "q":

(WCMATCH "Bloques" "*q*") devuelve T

— Detectar si una cadena no contiene ninguna letra "q":

(WCMATCH "Bloques" "~*q*") devuelve nil

— Detectar si una cadena contiene una coma (hay que indicar el literal de la coma):

(WCMATCH "Bloques,armario" "*’,*") devuelve T

— Detectar si una cadena comienza con la letra "B" o "b":

(WCMATCH "Bloques" "B*,b*") devuelve T

— Detectar si una cadena comienza por un carácter en mayúscula (cualquiera):

(WCMATCH "Bloques" "[A-Z]*") devuelve T

(READ [cadena])

Veamos una función muy útil. READ devuelve la primera expresión de la cadena indicada. Si la cadena no contiene ningún paréntesis y es un texto con espacios en blanco, READ devuelve el trozo de texto hasta el primer espacio (en general será la primera palabra del texto).

Si la cadena contiene paréntesis, se considera su contenido como expresiones en AutoLISP, por lo que devuelve la primera expresión. Se recuerda que los caracteres especiales que separan expresiones en AutoLISP son: espacio blanco, (, ), ’, " y ;. A continuación se ofrecen unos ejemplos:

(READ "Buenos días") devuelve BUENOS
(READ "Hola;buenas")
devuelve HOLA
(READ "Estoy(más o menos)bien"
devuelve ESTOY

Hay un aspecto muy importante que no debemos pasar por alto, y es que READ examina la cadena de texto pero analiza su contenido como si fueran expresiones AutoLISP. Por ello devuelve no una cadena de texto, sino una expresión de AutoLISP. De ahí que los ejemplos anteriores devuelvan un resultado que está en mayúsculas.

Y es que la utilidad real de READ no es analizar contenidos textuales, sino expresiones de AutoLISP almacenadas en cadenas de texto. Por ejemplo:

(READ "(setq x 5)") devuelve (SETQ X 5)
(READ "(SetQ Y (* 5 3)) (SetQ Z 2)")
devuelve (SETQ Y (* 5 3))

Es decir que devuelve siempre la primera expresión AutoLISP contenida en la cadena de texto. Si sólo hay una devolverá esa misma.

Estas expresiones pueden ser posteriormente evaluadas mediante la función EVAL cuya sintaxis es:

(EVAL expresión)

Esta función evalúa la expresión indicada y devuelve el resultado de dicha evaluación. Así por ejemplo:

(EVAL (SETQ x 15))

devuelve

15

Esto equivale a hacer directamente (SETQ x 15), por lo que parece que en principio no tiene mucho sentido. Y es que la función EVAL únicamente cobra sentido al utilizarla junto con la función READ.

Vamos a ver un ejemplo que ilustra perfectamente el funcionamiento de READ y EVAL juntos. Aunque la verdad es que no es un ejemplo muy práctico, ya que requiere conocimientos de AutoLISP por parte del usuario del programa, pero examinémoslo (más adelante se mostrará otro ejemplo mejor). Además este programa nos ayudará a afianzar conocimientos ya aprehendidos:

(DEFUN datos_curva ( / mens fun fundef pini pfin y1)
--(IF fun0 () (SETQ fun0 ""))
--(SETQ mens
--(STRCAT "Expresión de la función en X <" fun0 ">: "))
--(IF (= "" (SETQ fun (GETSTRING T mens))) (SETQ fun fun0))(TERPRI)
--(SETQ fundef (STRCAT "(defun curvaf (x)" fun ")"))
--(EVAL (READ fundef))
--(INITGET 1)
--(SETQ pini (GETPOINT "Inicio de curva en X: "))(TERPRI)
--(SETQ x1 (CAR pini) yb (CADR pini))
--(SETQ y1 (+ yb (curvaf x1)))
--(SETQ p1 (LIST x1 y1))
--(SETQ fun0 fun)
--(SETQ orto0 (GETVAR "orthomode")) (SETVAR "orthomode" 1)
--(INITGET 1)
--(SETQ pfin (GETPOINT pini "Final de curva en X: "))(TERPRI)
--(SETQ xf (CAR pfin))
--(WHILE (= xf x1)
----(PROMPT "Deben ser dos puntos diferentes.")(TERPRI)
----(INITGET 1)
----(SETQ pfin (GETPOINT pini "Final de curva en X: "))(TERPRI)
----(SETQ xf (CAR pfin))
--)
--(INITGET 7)
--(SETQ prx (GETREAL "Precisión en X: "))(TERPRI)
--(IF (< xf x1) (SETQ prx (- prx)))
--(SETQ n (ABS (FIX (/ (- xf x1) prx))))
)

(DEFUN curva (/ x2 y2 p2)
--(COMMAND "_pline" p1)
--(REPEAT n
----(SETQ x2 (+ x1 prx))
----(SETQ y2 (+ yb (curvaf x2)))
----(SETQ p2 (LIST x2 y2))
----(COMMAND p2)
---- (SETQ x1 x2 p1 p2)
--)
)

(DEFUN ult_curva (/ p2 yf)
--(SETQ yf (+ yb (curvaf xf)))
--(SETQ p2 (list xf yf))
--(COMMAND p2 "")
)

(DEFUN C:Curva (/ xf yb prx p1 n x1)
--(SETVAR "cmdecho" 0)
--(SETQ refnt0 (GETVAR "osmode"))(SETVAR "osmode" 0)
--(COMMAND "_undo" "_begin")
--(datos_curva)
--(curva)
--(ult_curva)
--(COMMAND "_undo" "_end")
--(SETVAR "osmode" refnt0)(SETVAR "cmdecho" 1)(PRIN1)
)

(PROMPT "Nuevo comando CURVA definido.")(PRIN1)

El programa dibuja el trazado de la curva de una función cualquiera del tipo y = f (x). Para ello se solicita al usuario la expresión de la curva, que habrá de introducir con el formato de una expresión de AutoLISP; por ejemplo (+ (* 5 x x) (- (* 7 x)) 3) se correspondería con la función y = 5x2 – 7x + 3. El programa también solicita el punto inicial y final de la curva, así como el grado de precisión de la misma, ya que se dibujará con tramos rectos de polilínea. Esta precisión viene a ser la distancia entre puntos de la polilínea.

El primer paso del programa consiste en desactivar el eco de los mensajes, guardar en refnt0 el valor de los modos de referencia activados (variable OSMODE) para luego restaurarlo, y poner dicha variable a 0, y colocar una señal de inicio del comando DESHACER. Tras ejecutar todas la funciones, se coloca una señal de fin y todo esto para que se puedan deshacer todas las operaciones del programa con un solo H o un solo DESHACER.


Esto es una práctica normal en los programas AutoLISP. Lo que ocurre es que los programas realizan una serie de ejecuciones de comandos de AutoCAD pero en el fondo, todo se encuentra soterrado transparentemente bajo un único comando. Si no estuviéramos conformes con el resultado de una ejecución de un programa, al utilizar el comando
H sólo se desharía el último comando de la serie de comandos de la rutina. De la forma explicada se deshace todo el programa.

Lo primero que realiza el programa, tras lo explicado, es comprobar, con una función IF, si la variable fun0 contiene alguna expresión o no. La forma de realizarlo es similar a un ejemplo de WHILE que ya se explico. En esta variable se guardará la última expresión introducida por el usuario y se utilizará como valor por defecto en la solicitud (siguientes líneas).

Lo que hace el IF es comprobar si fun0 devuelve T o nil. Si devuelve T es que contiene algo, por lo que no hace nada (lista vacía ()). Por el contrario, si devuelve nil es que está vacía, es decir, es la primera vez que se ejecuta el programa, por lo que la hace igual a una cadena vacía "". Esto se realiza así para que al imprimir el valor por defecto en pantalla, no se produzca ningún error al ser dicha variable nil. Es un método muy manido en la programación en AutoLISP.

NOTA: Nótese que la variable fun0 no ha sido declarada como local en los argumentos de DEFUN. Esto es debido a que necesita ser global para guardarse en memoria y utilizarse en todas la ejecuciones del programa.

A continuación, el programa presenta el mensaje de solicitud de la función en la línea de comandos. Por defecto se presentará la última función introducida (fun0) si existe, si no los corchetes angulares estarán vacíos, pero no habrá ningún error. La manera de presentar este mensaje es mediante una concatenación de cadenas y un posterior GETSTRING sin texto. La función introducida por el usuario se guarda en fun. Luego, con el IF siguiente nos aseguramos de darle a fun el valor de fun0 si fun es igual a una cadena vacía, es decir si se ha pulsado INTRO para aceptar la función por defecto.

Seguidamente se forma, mediante una concatenación de cadenas, la función completa, añadiéndole un (DEFUN CURVAF (X) por delante y un ) por detrás. De esta manera tendremos una función de usuario evaluable por AutoLISP.

NOTA: Esta manera de definir funciones con una variable asociada se expone al final de esta explicación del ejercicio.

A continuación se evalúa mediante EVAL la función contenida en la cadena fundef que se lee con la función de AutoLISP READ. El resultado es que se ejecuta el DEFUN y la función curvaf queda cargada en memoria para su posterior utilización.

Ahora se pide el punto de inicio de la curva en X, y se capturan sus coordenadas X e Y en las variables x1 e yb mediante las funciones CAR y CADR. Inmediatamente se calcula el inicio en Y (y1) llamando a la recién creada función curvaf y se guarda el punto como una lista de sus coordenadas (LIST) en p1. Después se guarda en fun0 el valor de fun para que en próximas ejecuciones del programa aparezca como opción por defecto.

A continuación se guarda en orto0 el valor de ORTHOMODE —para después restaurar— y se establece a 1 para activarlo. De esta forma se indica que la curva se trazará con una base horizontal. Se pregunta por la coordenada X final y se introduce el control del WHILE para que las coordenadas X inicial y final sean diferentes. Se restablece le valor de ORTHOMODE.

Por último en cuestión se solicitud de datos se solicita la precisión en X. Si el punto final está a la izquierda del inicial se establece la precisión negativa. El programa calcula el número de tramos de polilínea que almacena en n. FIX toma el valor entero del cociente; este valor es el número de tramos completos. Para dibujar el último tramo con intervalo incompleto se utiliza la función ult-curva.

A continuación, y ya en la función curva, se produce el dibujado de los tramos completos de la curva y, en la función ult-curva, del último tramo incompleto. Fin de la aplicación.

Llegados a este punto toca explicar la nueva manera de definir funciones de usuario con DEFUN. Veamos el siguiente ejemplo:

(DEFUN Seno (x)
--(SETQ xr (* PI (/ x 180.0)))
--(SETQ s (SIN xr))
)

Como vemos, este ejemplo utiliza una variable global, pero que luego es utilizada como argumento de la operación cociente sin estar definida. Esto no es del todo cierto, la variable está definida en el DEFUN, lo que ocurre es que no tiene valor. Este tipo de variables se denominan asociadas, ya que se asocian a una expresión.

Así, al ejecutar este programa desde AutoCAD no podríamos hacer simplemente:

(seno)

ya que produciría un mensaje de error, sino que habría que introducir un valor directamente a su variable asociada, por ejemplo:

(seno 90)

lo que calcularía el seno de 90 grados sexagesimales. Veamos otro ejemplo:

(DEFUN Suma (x y z)
--(SETQ Sum (+ x y z))
)

Con este ejemplo habríamos de escribir en la línea de comandos, por ejemplo:

(suma 13 56.6 78)

 

10ª fase intermedia de ejercicios

· Realizar un programa que facilite la edición de los archivos ASCII de AutoLISP. La rutina solicitará el nombre del archivo que se desea editar, proponiendo por defecto el último editado que se aceptará pulsando INTRO. Tras esto, el programa correrá el Bloc de notas de Microsoft (suministrado con Microsoft Windows) con el fichero especificado abierto.

· Realizar un programa que sea capaz de distribuir un texto introducido por el usuario en forma de arco: alrededor de un centro y con un radio especificado. El texto se generará en sentido horario.

 

 

ONCE.13. ÁNGULOS Y DISTANCIAS

Tras el estudio de cadenas vamos a estudiar un pequeño grupo de funciones que nos permiten manejar dos de los tipos de datos más utilizados por AutoCAD: los ángulos y las distancias. Recordemos aquí que dentro de AutoLISP los ángulos se miden siempre en radianes (como en casi todos los lenguajes de programación existentes).

Comenzamos por una función que se encarga de medir ángulos. Esta función es:

(ANGLE punto1 punto2)

ANGLE devuelve el ángulo determinado por la línea que une los dos puntos especificados (punto1 y punto2) y la dirección positiva del actual eje X en el dibujo. Así pues, entre punto1 y punto2 se traza una línea imaginaria y, el ángulo es el formado por esa línea con respecto al eje X positivo.

Como sabemos, el ángulo se mide en radianes y su sentido positivo es el antihorario o trigonométrico. Veamos un pequeño ejemplo:

(SETQ Inicio (GETPOINT "Primer punto: "))
(SETQ Final (GETPOINT Inicio "Segundo punto: "))
(SETQ Ang (ANGLE Inicio Final))

Para pasar este valor a grados sexagesimales, como comentamos ya, habría que hacer:

(SETQ AngSex (/ (* 180 Ang) PI))

Vemos que es una función muy similar a GETANGLE o GETORIENT. La diferencia estriba en que estas dos solicitan un ángulo, ya sea marcando dos puntos para calcularlo o por teclado, y ANGLE calcula el ángulo entre dos puntos. Si se indican dos puntos en pantalla con GETORIENT o GETANGLE el resultado es el mismo que con dos GETPOINT y la función ANGLE.

Es importante tener cuidado a la hora de introducir ambos puntos, argumentos de la función ANGLE. El orden en que sean introducidos determina la medida de un ángulo que no coincidirá en absoluto si se indica su posición de manera inversa. Así por ejemplo, si en el caso anterior se hubiera escrito (SETQ Ang (ANGLE Final Inicio)), el ángulo devuelto no se correspondería con el que se devuelve al escribir (SETQ Ang (ANGLE Inicio Final)). Esto mismo ocurría con GETANGLE y GETORIENT.

NOTA: Si los puntos introducidos son en 3D, se proyectan ortogonalmente en el plano XY actual.

(DISTANCE punto1 punto2)

Esta función devuelve la distancia 3D entre los dos puntos especificados. Lógicamente, con DISTANCE es indiferente el orden de introducción de puntos. Funciona de la misma manera, con dos GETPOINT, que GETDIST. Pero DISTANCE se puede utilizar para calcular la distancia entre dos puntos cualesquiera del proceso de un programa, es decir, que no hayan sido solicitados directamente al usuario. Veamos un ejemplo:

(DISTANCE (GETPOINT "Primer punto: ") (GETPOINT "Segundo punto: "))

El valor devuelto por DISTANCE es un número real, distancia 3D entre ambos puntos de acuerdo a sus coordenadas en el SCP actual.

Las unidades son siempre decimales, independientemente de la configuración de unidades actual en el dibujo. Si uno de los puntos especificado es 2D (no se indica su coordenada Z), se ignorará la coordenada Z del otro punto y se devolverá una distancia 2D. Evidentemente si los dos puntos son 2D, la distancia es también 2D.

(POLAR punto ángulo distancia)

La función POLAR devuelve un punto obtenido mediante coordenadas relativas polares a partir del punto especificado, es decir, se devuelven las coordenadas de un punto. Desde punto se lleva distancia en la dirección marcada por ángulo. Como siempre, el ángulo introducido se considera en radianes y positivo en sentido trigonométrico. Aunque el punto introducido como argumento pueda ser 3D, el valor del ángulo (argumento también) se toma siempre respecto al plano XY actual.

Veamos un pequeño programa ejemplo de POLAR:

(DEFUN C:Balda (/ Punto1 Punto2 Ortho0 Dist)
--(SETQ Ortho0 (GETVAR "orthomode")) (SETVAR "orthomode" 1)
--(SETQ Punto1 (GETPOINT "Primer punto de la balda: ")) (TERPRI)
--(SETQ Punto2 (GETCORNER Punto1 "Segundo punto de la balda: ")) (TERPRI)
--(COMMAND "_rectang" Punto1 Punto2)
--(SETQ Dist (GETDIST Punto1 "Distancia a la siguiente balda: ")) (TERPRI)
--(COMMAND "_select" "_l" "")
--(WHILE (/= Dist nil)
----(COMMAND "_copy" "_p" "" Punto1 (POLAR Punto1 (/ PI 2) Dist))
----(SETQ Dist (GETDIST Punto1 "Distancia a la siguiente balda: "))
--)
--(SETVAR "orthomode" Ortho0)
)

Este programa dibuja baldas a distancias perpendiculares a la horizontal indicadas por el usuario. Tras el comienzo de la función se guarda el valor del modo Orto para restaurarlo posteriormente y se establece como activado. Se pregunta por el primer y segundo punto de la diagonal del rectángulo que formará la primera balda. Una vez hecho esto, se dibuja la balda.

La siguiente fase consiste en copiar dicha balda las veces que se necesite en perpendicular. Para ello se pregunta por la distancia a la siguiente balda; el punto de base siempre será la primera esquina dibujada de la primera balda. A continuación se establece el último objeto dibujado (la primera balda) como conjunto de selección para recurrir a él después como previo.

Ya dentro del bucle se van copiando baldas a los puntos designados por el usuario cada vez. Para ello se utiliza la función POLAR. Como punto de inicio se utiliza siempre el de la esquina primera de la primera balda —como ya se ha dicho—, como ángulo PI / 2, es decir, 90 grados sexagesimales, y como distancia la que cada vez indique el usuario (variable Dist).

De este programa se sale pulsando INTRO cuando se nos pregunte por una distancia. Esto lo controla el bucle WHILE de la forma que ya se ha explicado alguna vez. En el momento en que se pulse INTRO, Dist será igual a nil y WHILE no continuará. Se saldrá del bucle y se restablecerá el valor original de Orto para acabar.

Veamos la última de este tipo de funciones. Es INTERS y se utiliza para obtener puntos por intersección entre dos líneas. No es exactamente una función que calcule ángulos o distancias, pero por su similitud de funcionamiento con ellas se ha incluido aquí. Su sintaxis es:

(INTSERS punto1 punto2 punto3 punto4 [prolongación])

Esta función toma los puntos punto1 y punto2 como extremos de una línea (aunque no lo sean), los puntos punto3 y punto4 como extremos de otra, y calcula el punto intersección de ambas, el cual devuelve. Veamos un ejemplo:

(INTERS ’(10 10) ’(20 20) ’(15 10) ’(0 50))

esto devuelve

(13.6364 13.6364)

que es el punto intersección.

El argumento prolongación es optativo. Si su valor es nil, la función INTERS considera las líneas como infinitas y devuelve su punto de intersección no sólo entre los límites indicados, sino también en su prolongación (si se cortan evidentemente). En este caso todas las líneas 2D tendrían intersección, salvo que fueran paralelas.

Si prolongación no se indica o su valor es diferente de nil, entonces el punto de intersección sólo se devuelve si se encuentra entre los límites indicados.

Si las rectas se cortan en su prolongación pero no está indicado el parámetro necesario, o si no se cortan de ninguna manera, INTERS devuelve nil. Veamos unos ejemplos:

(INTERS ’(10 10) ’(20 20) ’(15 10) ’(20 0)) devuelve nil
(INTERS ’(10 10) ’(20 20) ’(15 10) ’(20 0) nil)
devuelve (13.3333 13.3333)
(INTERS ’(10 10) ’(20 20) ’(15 10) ’(20 0) T)
devuelve nil
(INTERS ’(10 10) ’(20 20) ’(15 10) ’(20 0) (/= 2 2))
devuelve (13.3333 13.3333)

Hay que tener cuidado en indicar los cuatro puntos en el orden correcto, pues en caso contrario, las líneas cuya intersección calcula INTERS serán diferentes. Evidente.

 

11ª fase intermedia de ejercicios

· Créese un nuevo Modo de Referencia a Objetos que se "enganche" de puntos medios virtuales, es decir de puntos medios cualesquiera entre otros dos puntos que se habrán de señalar.

· Realizar un programa que dibuje números de marca de despieces. Se solicitará el diámetro del punto de señalización, el diámetro del círculo de marca, los puntos de situación y el número de marca. Tras esto, se dibujará respetando dichos datos.

 

ONCE.14. RUTINAS DE CONTROL DE ERRORES

Se va a explicar bajo esta sección una parte muy importante de la programación en cualquier lenguaje, sólo que aquí orientada al lenguaje que nos ocupa, orientada a AutoLISP. Esta parte a la cual nos referimos es el tratamiento de errores en un programa.

Con todo lenguaje de programación existe una función muy importante que debe realizar un programador a la hora de diseñar su aplicación. Esta función es la que se refiere al depuramiento o depuración de un programa, es decir, a la total eliminación de los errores de sintaxis o programación que pudiera contener un programa. Una vez hecho esto se puede decir que el programa funciona correctamente, pero ¿es esto cierto? Una depuración exhaustiva no consiste únicamente en revisar línea por línea de código buscando errores de sintaxis, variables mal declaradas, etc. Al programa hay que lograr ponerlo bajo las condiciones más duras de trabajo, hay que hacer que ejecute su listado al borde de los límites y más allá. En definitiva, hay que suponer todos los casos con los que se va encontrar un futuro usuario al ejecutar la aplicación.

Entre todos estos casos a los que nos referimos existen multitud de situaciones inestables que producen error en un programa y hacen que éste aborte su ejecución inesperadamente. Estas situaciones son las que debe prever el programador.

Un programa puede llegar a abortar no por un mal diseño de su código, sino simplemente porque se intente buscar un archivo para ser abierto en la unidad de disco flexible, por ejemplo, y el disco no esté introducido. El programador no puede adivinar la voluntad "maliciosa" de un usuario que no desea introducir el disco en la disquetera para que el programa "casque", o el olvido, o simplemente el despiste del manejador del programa. Sin embargo puede suponer que el caso puede producirse e introducir una rutina que controle dicho error y no deje abortar al programa.

En AutoLISP el problema es un poco diferente; no disponemos de las instrucciones o funciones potentes de control de errores que disponen otros lenguajes de programación. Y es que con AutoLISP siempre se diseñarán aplicaciones, rutinas, comandos, etcétera que corran bajo AutoCAD, y que un programa nuestro aborte no quiere decir que vaya a abortar AutoCAD. Simplemente habrá sido un comando fallido.

Examinemos, por ejemplo, qué ocurre cuando intentamos extruir una polilínea abierta. Tras solicitarnos todos los datos pertinentes, AutoCAD intenta extruir el objeto y, cuando llega a la conclusión de que no puede, nos muestra un mensaje de error que nos indica la imposibilidad de realizar esa operación con ese objeto. Podría habernos dicho que no podía ser al designar la polilínea, pero no, ha ocurrido al final. ¿Qué es lo que ha sucedido internamente? Pensemos en ello.

AutoCAD contiene un comando interno que posibilita la extrusión de objetos planos con superficie para convertirlos en sólidos. Este comando responde a un programa que ejecuta una serie de operaciones hasta llegar al resultado final. Si nosotros designamos, como en este caso, un objeto que no puede ser extruído, AutoCAD "se lo traga" en un principio, es decir, no comprueba si el objeto designado es factible de ser extruído o no. Pero al realizar las operaciones pertinentes un error (evidente) se produce en su código interno. El caso es que este error no se nos materializa de forma escandalosa haciendo que aborte AutoCAD o que el sistema quede inoperante —que así podría haber sido si no estuviera controlado—, sino que simplemente nos advierte "amablemente" que a susodicho objeto no se le puede aplicar una extrusión. Y acaba la orden.

Este es un buen ejemplo del control de errores que deberemos realizar nosotros. En AutoLISP no pretenderemos que un comando creado no aborte, porque siempre lo hará si encuentra algún problema, sino que procuraremos canalizar ese error para que no parezca tan grave o aparatoso y simplemente acabe de manera "limpia" el programa o, por ejemplo, nos ofrezca la posibilidad de volver a ejecutar el comando (que no es lo normal).

Veremos por ejemplo que con Visual Basic el control de errores será mucho más exhaustivo y real.

NOTA: Los comandos y aplicaciones que realicemos para AutoCAD 14 en AutoLISP siempre deben ser lo más parecido a los propios comandos y aplicaciones nativos del programa. Tanto en solicitud de opciones, como en control de errores, como en diseño de cuadros de diálogo, debemos guardar esa línea tan característica que distingue a AutoCAD de todos los demás.

Veamos pues la manera de aplicar esto con AutoLISP.

 

ONCE.14.1. Definir una función de error

La manera de definir una función de error en AutoLISP es, precisamente, con la función para definir funciones de usuario, con DEFUN. La función que concretamente hemos de definir no puede ser cualquiera, sino que ha de ser una concreta llamada *error*. La sintaxis para esta función especial se corresponde con cualquier definición de función, únicamente hemos de saber que el argumento siempre será la declaración de una variable global que guardará el texto del mensaje devuelto por AutoLISP en un evento de error.

La función *error* es una función predefinida de manipulación de errores. Al comenzar una sesión de dibujo, esta función tiene un tratamiento de errores por defecto. Cada vez que ocurre un error al ejecutarse un programa AutoLISP, se realiza el tratamiento incluido en ella. Lo que vamos a hacer nosotros simplemente es redefinirla para personalizar dicho tratamiento a nuestro gusto. Veamos un ejemplo:

(DEFUN *error* (cader)
--(IF (= cader "tipo de argumento erróneo") (err_arg))
)

En este ejemplo, al producirse un error, AutoLISP suministra el texto con su descripción como argumento a la función *error*. En este caso la variable cader almacena el valor de ese texto. Se examina haber si esa cadena es "tipo de argumento erróneo" y, en caso afirmativo se llama a una determinada función de tratamiento, en este caso err_arg, que hará lo que tenga que hacer.

Un tratamiento estándar sería el siguiente código:

(DEFUN *error* (Cadena)
--(PRINC "Error: ") (PRINC Cadena) (TERPRI)
--(PROMPT "*No válido.*") (PRIN1)
)

Cuando se produce un error de AutoLISP en un programa, éste queda abortado y se nos devuelve la parte del listado del programa donde se ha producido dicho error, además de todas las funciones que engloban esa parte, es decir, hasta el paréntesis más externo que es normalmente un DEFUN. Este listado puede resultar molesto y desagradable a los ojos del usuario. Una rutina sencilla para evitarlo es ésta expuesta aquí.

Al producirse un error, el texto de ese error se escribe con PRINC (que ya veremos en su momento) y, en lugar de ofrecer el listado, se ofrece un mensaje *No válido*. En general será necesario realizar un control más profundo como el que vamos a explicar ahora.

Sea el ejemplo siguiente, ya explicado anteriormente pero un poco mejorado con detalles que ya conocemos:


(DEFUN Aro (/ Centro Radio Grosor Rint Rext Dint Dext)
--(INITGET 1)
--(SETQ Centro (GETPOINT "Centro del aro: ")) (TERPRI)
--(INITGET 7)
--(SETQ Radio (GETDIST Centro "Radio intermedio: ")) (TERPRI)
--(INITGET 7)
--(SETQ Grosor (GETDIST "Grosor del aro: ")) (TERPRI)
--(INITGET "Hueco Relleno")
--(SETQ Op (GETKWORD "Aro Hueco o Relleno (<H>/R): ")) (TERPRI)
--(IF (OR (= Op "Hueco") (= Op \n))
----(PROGN
------(SETQ Rint (- Radio (/ Grosor 2)))
------(SETQ Rext (+ Radio (/ Grosor 2)))
------(COMMAND "_circle" Centro Rext)
------(COMMAND "_circle" Centro Rint)
----)
----(PROGN
------(SETQ Dint (* (- Radio (/ Grosor 2))2))
------(SETQ Dext (* (+ Radio (/ Grosor 2))2))
------(COMMAND "_donut" Dint Dext Centro "")
----)
--)
)

(DEFUN C:Aro ()
--(SETVAR "cmdecho" 0)
--(COMMAND "_undo" "_begin")
--(Aro)
--(COMMAND "_undo" "_end")

--(SETVAR "cmdecho" 1)
--(PRIN1)
)

(TERPRI)
(PROMPT "Nuevo comando Aro definido.") (PRIN1)

Este programa dibuja aros huecos o rellenos, según se indique. Mientras todo funcione correctamente y los datos introducidos no sean descabellados, no habrá problema alguno. Pero probemos a pulsar ESC cuando se nos pida un dato. El resultado en línea de comandos será el siguiente, por ejemplo:

error: Function cancelled

(COMMAND "_circle" CENTRO REXT)
(PROGN (SETQ RINT (- RADIO (/ GROSOR 2))) (SETQ REXT (+ RADIO (/ GROSOR 2)))
(COMMAND "_circle" CENTRO REXT) (COMMAND "_circle" CENTRO RINT))
(IF (OR (= OP "Hueco") (= OP \N)) (PROGN (SETQ RINT (- RADIO (/ GROSOR 2)))
(SETQ REXT (+ RADIO (/ GROSOR 2))) (COMMAND "_circle" CENTRO REXT) (COMMAND
"_circle" CENTRO RINT)) (PROGN (SETQ DINT (* (- RADIO (/ GROSOR 2)) 2)) (SETQ
DEXT (* (+ RADIO (/ GROSOR 2)) 2)) (COMMAND "_donut" DINT DEXT CENTRO "")))
(ARO)
(C:ARO)

Diameter/<Radius> <19.6581>:

NOTA: Aquí se trabaja con los mensajes en inglés, pero con la versión castellana el modo de hacer es el mismo.

Vemos que el programa ha sido abortado. El texto de error que lanza AutoLISP es Function cancelled (detrás de error:). Estos son los mensajes que guardará la variable asociada a la función de control de errores. Vamos pues a intentar controlar este error.

A la función que define el comando de AutoCAD (C:Aro) le vamos a añadir la línea siguiente: (SETQ error0 *error* *error* ControlErrores), justo detrás de la desactivación del eco de la línea de comandos (variable CMDECHO) por razones obvias. Con esto lo que hacemos es, primero guardar la definición predeterminada de la función predefinida *error* (en error0) para volver a restaurarla después; así nos aseguramos de dejarlo todo como estaba. Y segundo, una vez salvaguardado el contenido de esta función, asignarle el valor de la nueva función de control errores ControlErrores, que enseguida definiremos. Todo ello lo hacemos en un solo SETQ, que no despiste, lo podríamos haber hecho en dos.

Como siguiente paso evidente definiremos la nueva función ControlErrores así:

(DEFUN ControlErrores (CadenaError)
--(SETQ *error* error0)
--(IF (= CadenaError "Function cancelled")
----(PROMPT "La función ha sido interrumpida por el usuario.")
--)
--(TERPRI)
--(PRIN1)
)

De esta manera asociamos la variable CadenaError a la función, lo que hará que se guarde en dicha variable el contenido de la cadena de error lanzada por AutoLISP (si un error se produjera). A continuación restauramos el valor de la función predefinida *error*. Después comparamos el valor de CadenaError con el texto que proporciona AutoLISP si se presiona ESC a una petición y, si el valor es cierto, se muestra el mensaje del PROMPT. Por fin, realizamos un salto de línea con TERPRI para que el *Cancel* que devuelve AutoLISP no quede pegado a nuestro mensaje y acabamos con PRIN1.

Con esta función habremos controlado la salida de una pulsación de ESC. Podremos deducir fácilmente cómo hacer para que la pulsación de ESC devuelva únicamente el mensaje por defecto de AutoCAD, es decir el *Cancel*, sin nada más (como ocurre con los comandos del programa).

Si quisiéramos controlar así todos los errores, la rutina sería un tanto extensa. Por ello, lo que siempre se suele hacer es definir una rutina de tratamiento como la que sigue, por ejemplo:

(DEFUN ControlErrores (CadenaError)
--(SETQ *error* error0)
--(IF (= CadenaError "quit / exit abort")
----(PRINC "\nDatos no válidos. Fin del programa.")
----(PRINC (STRCAT "\nError: " CadenaError "."))
--)
--(TERPRI)
--(PRIN1)
)

De esta forma controlamos si el error ha sido producido por una función QUIT o EXIT de AutoLISP (que enseguida veremos) o por cualquier otra circunstancia. Si el caso es el primero se muestra un mensaje fijo y, si es el segundo se muestra el mensaje Error: y a continuación el propio error de AutoLISP. De esta manera tenemos cubiertas todas las posibilidades.

Estos mensajes se pueden personalizar para cada tipo de error o incluso definir diversas funciones de tratamiento de errores para cada función del programa, por ejemplo. Así, si sabemos que el usuario habrá de introducir una serie de datos que, al final, pueden producir una división entre 0, si este error ocurre el mensaje podría indicar específicamente que el programa no ha podido continuar porque se ha intentado realizar una división entre cero no permitida (algo parecido a lo que explicábamos que hacía EXTRUSION).

Pero aún nos queda algún problema por resolver. Si nos fijamos en el ejemplo del listado devuelto tras un mensaje de error (antes de controlar) que hemos proporcionado anteriormente, al final del listado vemos que AutoCAD reanuda el comando CIRCULO pidiendo nuevos datos. Con el control que hemos proporcionado ya no aparecería, pero fijémonos en el ejemplo siguiente:

 

(DEFUN C:EscribeHola ()
--(COMMAND "_text" "_s" "fantastic" "\\" "\\" "\\" "Hola")
)

Este pequeño ejemplo escribe Hola con el punto de inserción, la altura de texto y el ángulo de rotación proporcionados por el usuario (recordemos que "\\" es igual que pause, y espera en medio de un comando una introducción de un dato por el usuario). El problema es que si el estilo de texto FANTASTIC no existe el programa queda abortado pero AutoCAD sigue solicitando datos del comando TEXTO.

La manera de solucionar esto es incluyendo en la función de tratamiento de errores una llamada a la función COMMAND sin argumentos. De esta forma se cancela cualquier comando en curso (algo parecido a los caracteres ^C^C que incluíamos en macros de menús y botones de barras de herramientas). Así nuestra rutina de control de errores quedaría:

(DEFUN ControlErrores (CadenaError)
--(SETQ *error* error0)
--(IF (= CadenaError "quit / exit abort")
----(PRINC "\nDatos no válidos. Fin del programa.")
----(PRINC (STRCAT "\nError: " CadenaError "."))
--)
--(COMMAND)
--(TERPRI)
--(PRIN1)
)

Y otro problema que queda en el aire es la restauración de las variables cambiadas, así como la colocación de la marca de final del comando DESHACER. Cuando un programa queda abortado se cede el control a la función de tratamiento de errores y ya no vuelve a pasar por el resto de las funciones. Por eso todo aquello que hayamos variado habremos de restaurarlo antes de terminar. Una forma, y siempre dependiendo del programa, podría ser:

(DEFUN ControlErrores (CadenaError)
--(SETQ *error* error0)
--(IF (= CadenaError "quit / exit abort")
----(PRINC "\nDatos no válidos. Fin del programa.")
----(PRINC (STRCAT "\nError: " CadenaError "."))
--)
--(COMMAND "_undo" "_end")
--(SETVAR "blipmode" blip0) (SETVAR "osmode" refnt0)
--(SETVAR "cmdecho" 1)
--(COMMAND)
--(TERPRI)
--(PRIN1)
)

Así también, si el programa hubiera dibujado ya algo en pantalla antes de producirse el error, se podría hacer que lo deshiciera todo para no dejar rutinas a medias.

Nótese que hemos dicho que un el programa cede el control a la rutina de tratamiento de errores al producirse un error, no que aborte inesperadamente. Por lógica se puede deducir que podemos realizar un control tal que el programa, por ejemplo, vuelva a reiniciarse por completo o ceda el control a la función fallida para volver a introducir datos o lo que sea.

Como sabemos y ya hemos dicho, los comandos de AutoCAD no vuelven a repetirse si se da un error de estas magnitudes. Sí lo hacen a la hora de introducir datos, porque los vuelven a pedir si son erróneos, pero eso ya lo controlamos con los INITGET. Por lo tanto, lo más lógico será coincidir con la línea de AutoCAD y salir de la rutina, eso sí, de una forma controlada y vistosa.

Además de todo lo expuesto decir que la variable de sistema ERRNO almacena un valor cuando una llamada de función de AutoLISP provoca un error que AutoCAD detecta después. Las aplicaciones de AutoLISP pueden consultar el valor actual de ERRNO mediante GETVAR para obtener información de las causas de un error. A no ser que se consulte inmediatamente después de que una función AutoLISP informe de un error, el error que su valor indica puede ser engañoso. Esta variable siempre se inicializa a cero al empezar o abrir un dibujo.

NOTA: Si siempre utilizamos la misma rutina de control de errores, podemos escoger incluirla en el archivo ACAD.LSP para que siempre esté definida de la misma forma. La función de este archivo se estudiará en la sección ONCE.15..

NOTA: Al final de este MÓDULO existe una relación completa de todos los códigos de error existentes (variable ERRNO) y de todos los mensajes devueltos por AutoLISP (función *error*).

 

ONCE.14.2. Otras características del control de errores

Existen tres funciones en AutoLISP que también se utilizan a la hora de detectar y procesar un error. La primera de ellas es ALERT, cuya sintaxis es:

(ALERT mensaje)

ALERT presenta un sencillo letrero de diálogo con el mensaje de advertencia especificado en el argumento mensaje. El letrero sólo contiene un botón Aceptar que, al pulsarlo hará que desaparezca el cuadro. Ejemplo:

(ALERT "Procedimiento no permitido")

Incluyendo el código de control \n podemos separar el mensaje en varias líneas:

(ALERT "Procedimiento no\npermitido")

El número de líneas de estos cuadros de advertencia y su longitud dependen de la plataforma, el dispositivo y la ventana utilizada. AutoCAD trunca las cadenas cuya longitud supere el tamaño del cuadro advertencia dependiendo de la plataforma.

ALERT puede ser utilizado en el momento que se detecta un error para presentar una advertencia en pantalla y, por ejemplo, solicitar de nuevo los datos. Veamos un ejemplo:

(DEFUN Datos1 ()
--(SETQ PuntoInicio (GETPOINT "Punto de inicio: ")) (TERPRI)
--(SETQ Radio (GETDIST PuntoInicio "Radio:" )) (TERPRI)
)

(DEFUN Datos_Vuelt ()
--(IF (SETQ Vueltas (GETINT "Número de vueltas: "))
----()
----(PROGN
------(ALERT "Debe introducir un número de vueltas")
------(Datos_Vuelt)
----)
--)
)
...

(DEFUN C:Vuelt ()
--(Datos1)
--(Datos_Vuelt)
...
)

Las otras dos funciones que comentaremos son EXIT y QUIT. Sus respectivas sintaxis son:

(EXIT)

y

(QUIT)

Ambas producen el mismo efecto, esto es, interrumpen la aplicación actual en el momento en el que son leídas, devolviendo el mensaje de error quitar / salir abandonar (en inglés es quit / exit abort, como ya hemos visto).

Se pueden utilizar para acabar el programa directamente en el momento en que se detecte un error. Podremos utilizarlos de la manera siguiente, por ejemplo:

(DEFUN Datos1 ()
--(SETQ PuntoInicio (GETPOINT "Punto de inicio: ")) (TERPRI)
--(SETQ Radio (GETDIST PuntoInicio "Radio:" )) (TERPRI)
)

(DEFUN Datos_Vuelt ()
--(SETQ Vueltas (GETINT "Número de vueltas: "))
--(IF (< (SETQ Vueltas (GETINT "Número de vueltas: ")) 1)
----(QUIT)
--)
)
...

(DEFUN C:Vuelt ()
--(Datos1)
--(Datos_Vuelt)
...
)

Claro que habría que controlar la salida como ya hemos explicado.

NOTA: Normalmente estas funciones se utilizan también en la depuración de un programa. Detenemos la ejecución con QUIT o EXIT para evaluar distintas variables y demás.

 

 

 

12ª fase intermedia de ejercicios

· Programar una rutina AutoLISP que dibuje remates de tubos cortados en 2D. Se solicitarán los datos convenientes y se añadirá una rutina de control de errores.

· Realizar un programa con control de errores que dibuje puertas en planta cortando tabiques y muros. Solicitar los datos pertinentes.