Nawet najprostsze rzeczy można skomplikować, tak też jest w LISPie.
Prostą kreskę można narysować na trzy sposoby, każdy z nich ma swoje wady, niektóre mają nawet zalety. A sposoby są następujące:
- Używając funkcji command
- AutoLISP
- VisualLISP
COMMAND
Używanie funkcji command jest najprostszym sposobem tworzenia obiektów. Nazwę funcji wpisujemy command, następnie nazwę polecenia ZWCADa np. "_LINE" a dalej parametry typowe dla wywołanego polecenia ZWCADa, w przykładzie linii, jako kolejne parametry wpisujemy współrzędne punktów jako listy, żeby zakończyć polecenie konieczne jest dodanie na końcu pustego tekstu co będzie równoznaczne z naciśnięciem [ENTER] czyli zakończeniem działania funkcji.
W podobny sposób możemy utworzyć np. warstwę
Na przykładzie tym zobaczyć możemy, że command może zawierać całą sekwencję czynności, tutaj wywołane jest polecenie -layer, a kolejne wywoływane opcje powodują:
"n" - utworzenie nowej warstwy, po czym wpisujemy nazwę naszej nowej warstwy
"c" - kolor warstwy, której nazwę podajemy dalej
"s" - ustawia jako aktualną warstwę, której nazwę podajemy po opcji s
"" - konieczne jest jeszcze tylko zamknięcie polecenia, i nasza warstwa jest utworzona i ustawiona jako aktualna
Jak już wspomniałem, każde rozwiązanie ma swoje wady i zalety, wśród zalet tworzenia elementów przez command wyróżnić należy prostotę tego rozwiązania. Jeśli znamy polecenia ZWCADa, nie ma problemu, z tworzeniem funkcji automatyzującej rysowanie.
Wadą tego rozwiązania jest brak kontroli nad tym co się dzieje w czasie wykonywania skryptu. Command zawsze zwraca nil, niezależnie od tego, czy uda się poprawnie wykonać polecenie, czy nie. Dodatkowo, w historii poleceń wyświetlane są wszystkie polecenia, które się wykonuje, co w pewnym sensie ujawnia strukturę naszego programu, a to prowokuje do dłubania w naszym kodzie osoby, które niekoniecznie powinny to robić.
AutoLISP
W AutoLISP tworzenie elementów możemy przy użyciu funkcji entmake. jest bardziej skomplikowane od skryptów z użyciem poleceń, ale daje nam też większe możliwości. Spowodowane jest to tym, że używając tych mechanizmów mamy dostęp bezpośrednio do bazy danych rysunku. Każdy rysunek ma własną bazę danych zawierających narysowane elementy, warstwy, bloki, dane dodatkowe.
Drugą ważną zaletą entmake jest możliwość kontroli tego co się stanie. Jeśli polecenie się powiedzie, entmake zwróci listę definicji dla danego elementu, jeśli z jakichś przyczyn (najczęściej błędu programisty) nie uda się utworzyć takiego elementu, entmake zwróci nil.
Kod rysujący prostą linię wygląda następująco:
Efekt będzie równoważny naszemu przykładowi z tworzeniem takiej samej linii przez command. Entmake, daje nam o tyle większe możliwości, że można już teraz ustawić kolor, warstwę, typ linii, i wszystkie inne właściwości. Czego nie daje nam command. Używając funkcji command musielibyśmy ustawić aktualną warstwę, aktualny styl linii, kolor, grubość linii itp. a entmeke, proszę bardzo:
'(0 . "LINE") ; Określa nam jaki typ obiektu chcemy narysować
'(100 . "AcDbEntity") ; Tego nie trzeba rozumieć, po prostu ma być
'(100 . "AcDbLine") ; Nazwa klasy obiektu. dla każdego typu obiektu zawsze jest to samo
(cons 10 (list 0 0 0)) ; punkt początkowy
(cons 11 (list 10 10 10 )) ;punkt końcowy
(cons 8 "Warstwacommand") ; warstwa
'(48 . 1.00000) ;skala typu linii
'(62 . 1) ; kolor
'(6 . "CONTOUR") ;Typ linii
'(370 . 53) ; grubość linii
'(60 . 0) ; widoczność
)))
Małe przypomnienie: Tworząc listę definicji możemy używać dwu sposobów konstruowania list asocjacyjnych: przez '(x . y) lub (cons x y). Różnica taka, że w przypadku cons, y może być zmienną, przy '(x .y) y musi być stałą wartością.
W powyższym przykładzie widzimy, że już w momencie utworzenia elementu możemy ustawić wszystkie własności. Warto wspomnieć, że warstwa, na której zostanie utworzony element w przypadku entmake nie musi być warstwą aktualną. A już bezczelnością jest utworzenie linii i ustawienie jej jako niewidocznej '(60 . 1) . Po co? np. po to, żeby posłużyć się nią jako linią pomocniczą z której można odczytać np. punkty styczności czy przecięcia, a zaraz potem ją usunąć, użytkownik naszej nakładki nie musi być świadomy wszystkiego co dzieje się w programie, jeszcze by się przestraszył.
Rodzi się pytanie "Skąd mam wiedzieć jaki numerek (kod DXF) na ta właściwość, którą chcę ustawić?" Pomoc ZWCAD nie ma tego rozpisanego, jedno takie oprogramowanie podobne do ZWCADa to ma, ale nie powiem jakie :). Może interesujące dla kogoś będą inne przykłady. Linia już była, więc kolejno:
Okrąg
'(0 . "CIRCLE")'(100 . "AcDbEntity")'(100 . "AcDbCircle") ; to jest stałe dla okręgu
(cons 10 Center) ; środek jako lista np. (setq Center(list 10 10 10) )
(cons 40 Radius)) ; promień jako wartość typu real np. 10.0
))
Oczywiście można też dopisywać inne własności, warstwy, kolory itp. powyżej tylko to, co jest niezbędne do utworzenia okręgu
Polilinia
'(43 . 0) ; stała szerokość
(cons 90 4) ; Ilość punktów
(cons 70 1) ; czy polilinia jest zamknięta?
(cons 10 (list 5 5 0))
(cons 10 (list 15 5 0))
(cons 10 (list 15 15 0))
(cons 10 (list 5 15 0))
)))
Jeśli ktoś ma jeszcze wątpliwości po co są listy, to teraz jest to najłatwiej wyjaśnić. W powyższym przykładzie wpisaliśmy stałe wartości narożników naszej polilinii. A może warto byłoby zrobić listę w której użytkownik wskazywałby cztery punkty i na ich podstawie tworzona byłaby polilinia. Wyglądałoby to tak:
'(43 . 0)
(cons 90 (length Coords)) ; length zwraca ilość elementów w liście
(cons 70 1)
(cons 10 (car Coords))
(cons 10 (cadr Coords))
(cons 10 (caddr Coords))
(cons 10 (nth 3 Coords))
)))
Kreskowanie
To już chyba jest najtrudniejsze z podstawowych typów obiektów.
Sam tego nie używam, ponieważ uważam że jest na tyle skomplikowane, że stworzenie tego samego używając funkcji VisualLISP jest znacznie łatwiejsze, wiec tylko tak informacyjnie, żeby nie było że zaniedbałem.
Przykład wygląda tak:
(list (cons 0 "HATCH")'(100 . "AcDbEntity")'(100 . "AcDbHatch")
(cons 8 "0") (cons 62 256) (cons 10 (list 0.0 0.0 0.0)) (cons 210 (list 0.0 0.0 1.0)) (cons 2 "SOLID") ; nazwa wzoru
(cons 70 1) ; wypełnienie pełne (solid) czy wzorem
(cons 71 0) ; kreskowanie asocjatywne?
(cons 91 1) ; ilość pętli
(cons 92 1) ; typ pętli (wewnętrzna czy zewnętrzna)
(cons 93 1) ; ilość krawędzi
(cons 72 2) (cons 10 (list 0.0 0.0 0.0)) (cons 40 0.5)(cons 50 0)(cons 51 (* 2 pi) )(cons 73 0) ;kółko
(cons 97 0) ; ilość powiązanych obiektów 330 byłoby listą obiektów
(cons 75 1) ; wykrywanie wysp
(cons 76 1) ; typ wzoru
(cons 98 1) ; ilość seedpoints
(cons 10 (list 0.0 0.0 0.0)) ; seedpoint
(cons 470 "LINEAR"))))
93 oznacza ilość krawędzi. (dla okręgu 1, dla prostokąta 4) W zależności od tego co się tutaj ustawi inne opcje są różne, np. jeśli ustawimy
1 czyli kształt będzie kółkiem, to ustawiamy
10 = środek
40 = promień
50 = Kąt początkowy
51 = Kąt końcowy
73 = zgodnie czy przeciwnie do wskazówek zegara
4 czyli czworobok, wymaga, żeby cztery razy powtórzyć sekwencję (cons 72 1) (cons 10 (list X1 Y1 Z1)) (cons 11 (list X2 Y2 Z2))
gdzie
71 oznacza typ krawędzi, dostępne są tu wartości: 1 dla linie, 2 łuk kołowy, 3 łuk eliptyczny, 4 Spline
10 i 11 początek i koniec krawędzi.
76 typ wzoru: 0 = użytkownika, 1 = predefiniowana, 2 = dostosowane
98 ilosć punktów wewnątrz pętli
10 - punkty wewnętrzne pętli
Jeśli to jeszcze nie jest dość skomplikowane, to dodam, że nie tylko istotne jest który numer DXF oznacza jaką własność, ale jeszcze istna jest sekwencja ich ułożenia. po 10 po 72 to początek krawędzi, a 10 po 98 to współrzędne punktu wewnątrz pętli.
Przykład z kreskowaniem w kształcie kółka już jest, to dodam jeszcze przykład z trójkątem:
(list (cons 0 "HATCH")
'(100 . "AcDbEntity")
'(100 . "AcDbHatch")
(cons 8 "0")
(cons 62 256)
(cons 10 (list 0.0 0.0 0.0))
(cons 210 (list 0.0 0.0 1.0))
(cons 2 "SOLID")
(cons 70 1)
(cons 71 0)
(cons 91 1)
(cons 92 1)
(cons 93 3)
(cons 72 1)
(cons 10 (list 2.0 1.1547 0.0))
(cons 11 (list -2.0 1.1547 0.0))
(cons 72 1)
(cons 10 (list -2.0 1.1547 0.0))
(cons 11 (list 0 -2.3094 0.0))
(cons 72 1)
(cons 10 (list 0 -2.3094 0.0))
(cons 11 (list 2.0 1.1547 0.0))
(cons 97 0)
(cons 75 2)
(cons 76 1)
(cons 98 1)
(cons 10 (list 0.0 0.0 0.0))
(cons 470 "LINEAR")))
)
Warstwa
Używając entmake, można tworzyć nie tylko obiekty rysunkowe, ale również obiekty niegraficzne, takie jak warstwy, bloki, style tekstu itp. Przedstawię to na przykładzie warstwy:
'(0 . "LAYER") '(100 . "ZcDbSymbolTableRecord") ;O tablicach symboli może innym razem, na razie wystarczy wiedzieć, że to musi wyglądać właśnie w ten sposób
'(100 . "ZcDbLayerTableRecord")
(cons 2 nazwa)
'(70 . 64) ; flagi statusu
'(62 . 1) ; indeks koloru
'(290 . 1) ; czy warstwa ma być drukowana
'(370 . 15) ; grubość linii
'(6 . "Continuous") ; typ linii
)))
Jedna z istotnych tu spraw, to flagi statusu. Jest to informacja o stanie warstwy, to właśnie tu ustawiamy czy warstwa ma być: włączona, zamrożona itp.
wartości jakie możemy tu ustawić to:
- 1 = warstwa zamrożona
- 2 = warstwa zamrożona domyślnie w nowej rzutni
- 4 = warstwa jest zablokowana
- 16, 32 związane z warstwami z odnośników zewnętrznych, na razie możemy to pominąć
Jeśli 290 ustawione jest na 0, warstwa jest niedrukowana
W przypadku opcji 370 = grubości linii, dostępne są tylko określone wartości np. 15 jeśli chcemy linię grubości 0.15, 5 dla 0.05, 158 dla 1.58 itd.
Funkcjami AutoLISP można nie tylko tworzyć, ale też modyfikować obiekty. W skrócie proces można przedstawić następująco:
- Tworzymy element, lub pozwalamy użytkownikowi na jego wskazanie.
- Pobieramy jego definicję w postaci listy asocjacyjnej
- Modyfikujemy tą listę
- Uaktualniamy element zgodnie z listą
funkcja entmake jeśli zostanie wykonana prawidłowo zwróci nam listę definiującą obiekt. W zasadzie lista ta jest nam w tym przypadku nieużyteczne, jednak świadczy o tym, że utworzenie elementu zostało wykonane poprawnie. Więc nasza przykładowa funkcja (setq LiniaDef(entmake (list '(0 . "LINE") '(100 . "AcDbEntity") '(100 . "AcDbLine")(cons 10 (list 0 0 0)) (cons 11 (list 10 10 10 ))))) zwróci nam ((0 . "LINE") (100 . "AcDbEntity") (100 . "AcDbLine") (10 0 0 0) (11 10 10 10)). A właśnie tak nawiasem mówiąc jeśli ktoś zastanawia się jak sprawdzić wartość zmiennej, skoro nie jest dostępny debuger, odpowiedź jest prosta i sprowadza się do wywołania funkcji (print). Ale wracając do tematu, w zwróconej liście mamy spisane własności, nie mamy jednak żadnego identyfikatora na podstawie którego wiedzieć możemy którym obiektem operujemy. Rozwiązaniem jest użycie funkcji entlast
Proszę zapoznać się z poniższym przykładem:
(setq Ost_Ent(entlast))
(setq Ost_List (entget Ost_Ent))
(setq Ost_Popr_List (subst (cons 10 (list 0 20 0)) (assoc 10 Ost_List) Ost_List))
(setq PlineDef(entmod Ost_Popr_List))
Funkcja entlast zwraca ostatnio utworzony obiekt, powinno nam zwrócić coś podobnego do:
?Entity name: 8838d90
?
Jest to nazwa obiektu, na którym możemy dokonywać modyfikacji. Jednak aby te modyfikacje odniosły skutek, potrzebna jest nam pełna lista definicji, ją możemy odczytać z obiektu przez entget. nasz przykładowy entget zwróci nam:
((-1 . ) (0 . "LINE") (5 .
"144") (67 . 0) (8 . "0") (410 . "Model") (62 . 256) (6 . "ByLayer")
(370 . -1) (48 . 1.00000) (60 . 0) (39 . 0.000000) (10 0.000000
0.000000 0.000000) (11 10.0000 10.0000 10.0000) (210 0.000000 0.000000
1.00000))
W kolejnym kroku zamieniamy elementy wewnątrz listy definicji. (setq Ost_Popr_List (subst (cons 10 (list 0 20 0)) (assoc 10 Ost_List) Ost_List))
czytając kod LISP należy robić to od prawej strony do lewej, więc w kolejności wykonywało się będzie (assoc 10 Ost_List)
; zwróci nam pierwsze wystąpienie 10 w liście definicji linii(cons 10 (list 0 20 0))
; utworzy nam listę asocjacyjną (10 0 20 0)(subst X Y Lista)
; Podmienia w Liście jedną wartość na inną, tutaj wystąpienie Y zostanie zastąpnione przez X
czyli zwróci nam ((-1 . Entity name: 8838d90) (0 . "LINE") (5 . "144") (67 . 0) (8 . "0") (410 . "Model") (62 . 256) (6 . "ByLayer") (370 . -1) (48 . 1.00000) (60 . 0) (39 . 0.000000) (10 0 20 0) (11 10.0000 10.0000 10.0000) (210 0.000000 0.000000 1.00000))
Pozostaje nam jedynie aktualizować zmieniony obiekt, co robimy przy użyciu funkcji entmod a jej parametrem będzie zaktualizowana lista definicji obiektu.
Warto jeszcze dodać na koniec, że aktualizowanie obiektu nie musi być wykonane po każdej zmianie właściwości, czyli możemy pobrać definicję, zmienić w niej wiele właściwości, a na koniec raz aktualizować obiekt przez entmod.
Ostatnią kwestią w zakresie tworzenia i modyfikowania obiektów jest wskazywanie elementów do zmiany. W poprzednim przykładzie posługiwaliśmy się ostatnio utworzonym obiektem. Modyfikacje można też wykonywać na dowolnym obiekcie, który wskaże użytkownik. Pomocna jest nam w tym funkcja (entsel)
(setq Ost_List (entget Sel_Ent))
(setq Ost_Popr_List (subst (cons 62 5) (assoc 62 Ost_List) Ost_List))
(setq PlineDef(entmod Ost_Popr_List))
- entsel - wskaż obiekt (zwraca nazwę obiektu i punkt kliknięty) , car pobiera nazwę obiektu
- entget - pobiera definicję z nazwy obiektu
- Zamienia kolor (kod DXF 62) z istniejącego na niebieski, indeks koloru niebieskiego to 5
- entmod - aktualizuje obiekt
VisualLISP
No i dotarliśmy do tego, co można nazwać programowaniem, a w zasadzie programowaniem obiektowym.
Moim zdaniem jest to łatwiejsze niż używanie entmake. Nie wymaga uczenia się żadnych dziwnych numerów kodów, wystarczy wiedzieć czego się chce.
w ZWCAD VisualLISP jest dostępne od dawna, jednak w ograniczonym zakresie. W wersji 2010 dokonano znacznych zmian z tym zakresie i można już powiedzieć, że działa to w sposób zadowalający.
Zaczniemy może od najprostszej funkcji rysującej prostą kreskę:
(setq *ZCApp* (vlax-get-zwcad-object))
(setq *ActiveDocment* (vla-get-activedocument *ZCApp*))
(setq *MSpace* (vla-get-modelspace *ActiveDocment*))
(vl-load-com) - wczytuje do przestrzeni pamięci ZWCADa, funkcje pozwalające na programowanie obiektowe
(setq *ZCApp* (vlax-get-zwcad-object )) - pobiera obiekt ZWCADa jest to konieczne, jest to główny obiekt aplikacji i na nim wykonujemy wszystkie kolejne operacje. Przypisujemy ten obiekt do zmiennej globalnej *ZCApp*
(setq *ActiveDocment* (vla-get-activedocument *ZCApp*)) pobiera aktywny dokument z aplikacji i przypisuje do zmiennej *ActiveDocment*
(setq *MSpace* (vla-get-modelspace *ActiveDocment*)) pobiera przestrzeń modelu z aktywnego dokumentu-rysunku.
Już na tak prostych operacjach możemy zobaczyć pewną prawidłowość: żeby odczytać jakąś własność, lub obiekt z innego obiektu, możemy to zrobić, używając funkcji:
vla-get-... gdzie vla lub vlax to przedrostek wszystkich funkcji VisualLISP. get używamy, gdy chcemy odczytać informacje z obiektu. Gdy chcemy coś zapisać, używamy vla-put . Jeśli chcemy uruchomić funkcje będącą metodą obiektu używamy vlax-invoke-method W całym programowaniu obiektowym czyli VisualLISP, potrzebna jest znajomość jedynie około 10 funkcji i angielskie nazwy właściwości i metod obiektów. W tym zakresie dokumentacja ZWCADa jest moim zdaniem wystarczająco obszerna. Funkcje, które musimy poznać to:
- (vlax-put-property Obj 'Nazwawłaściwości [wartość]) ustawi własności o podanej nazwie wartiść podaną jako ostatni parametr
- (vlax-get-property Obj 'Nazwawłaściwości) zwróci nam wartość właściwości, którą chcemy odczytać
- (vlax-invoke-method Obj 'Metoda) wywoła funkcję=metodę którą chcemy uruchomić
Praktycznie to wygląda tak:
;lub w uproszczeniu
(setq Linia(vla-addLine *MSpace* (vlax-3d-point (list 0 0 0)) (vlax-3d-point (list 10 10 0))))
(vlax-put-property Linia 'Layer "Warstwa1")
(vla-putColor Linia 3)
vlax-invoke-method Object 'Metoda często można zamienić przez vla-Metoda Object jak w naszym przykładzie (vlax-invoke-method *MSpace* 'addLine...) i vla-addLine. Ma to na celu ułatwienie pracy programistom i nie ma wpływu na działanie. Moim zdaniem to jest przede wszystkim kwestia przyzwyczajenia.
Pojawiła się tu jedna nowa funkcja: vlax-3d-point jej zadaniem jest zamiana listy współrzędnych na variant. Jest to wymuszone przez funkcje addLine, która wymaga, żeby współrzędne podawane były jako variant.