Die ActiveX-Schnittstelle bietet für (fast) jedes geometrische Objekt,
das in der Geometriedatenbank von AutoCAD vorkommen kann, die Methode
IntersectWith an, mit deren Hilfe Schnittpunkte mit einem zweiten
Geometrie-Objekt berechnet werden können. Ausnahmen sind hier nur das
Papierbereichs-Ansichtsfenster PViewPort und das Polygon-Mesh, für diese
beiden Entities existiert die Methode nicht.
In diesem Kapitel geht es darum, eine (recht kleine) Funktion für den
Allgemeingebrauch in AutoLisp zu schaffen, die sich diese ActiveX-Methode
zunutze macht und die Schnittpunkt zwischen beliebigen Entities
berechnet, z.B. zwischen zwei Kreisen oder zwischen einem Bogen und
einer Linie. Aber auch das mühsame Berechnen der Schnittpunkte zweier
Polylinien oder Splines nimmt sie uns ab.
Alle verwendeten ActiveX-Aufrufe (sprich: vlax-...) werden innerhalb
der Funktion gekapselt. Nach Aussen hin verhält sich die Funktion also
wie eine 'ganz normale' Lisp-Funktion, und um sie zu verwenden, muss
man eigentlich auch keine Ahnung von ActiveX haben - aber trotzdem
wird es nichtschaden, wenn man auch versteht, was da vor sich geht.
ActiveX und AutoLisp sind von den Datentypen her völlig inkompatibel.
Daher müssen die AutoLisp-Entitynamen zunächst einmal in den Datentyp
ActiveX-Object konvertiert werden. Die Funktion
(vlax-ename->vla-object <ename>) erledigt das. Dann
haben wir zwei Möglichkeiten, die sich geringfügig in der Schreibweise
unterscheiden: Wir können mit
(vlax-invoke-method ...) die
Methode auf die beiden Objekte anwenden, oder wir benutzen die
Kurzschreibweise
(vla-IntersectWith ...). Das Problem bei der
kurzen Schreibweise ist, dass nirgendwo in der AutoLisp-Hilfe dokumentiert
ist, welche Funktionen da überhaupt existieren. Es sind zwar weit
über 1000 solcher Kurzfunktionen vorhanden, aber es existiert nicht
einmal eine Aufstellung darüber.
Da der Schreibaufwand mit
(vlax-invoke-method) eigentlich auch
nur ein paar Buchstaben mehr beträgt, bleiben wir also lieber bei dieser
Version. Mit dieser Funktion kann jede definierte Methode eines Objekts
aufgerufen werden. Falls nicht klar sein sollte, was eine Methode
überhaupt ist, hier eine ganz kurze Erklärung: In AutoLisp kennen wir
(bisher) nur Variablen und Funktionen, die nicht an ein bestimmtes
Objekt gebunden sind, sondern 'frei im Raum stehen' und auch aus jedem
Kontext heraus abrufbar sind. In der objektorientierten Programmierung
nennt man die zu einem Objekt gehörenden Funktionen Methoden, und die
zu einem Objekt gehörenden Variablen nennt man Properties.
Ein kurzes gedankliches Beispiel dazu: Der Umfang eines Kreises muss
sicherlich anders berechnet werden als der Umfang eines Rechtecks. In
klassischem Autolisp würden wir uns dafür zwei verschiedene Funktionen
definieren:
(defun kreisumfang(...))
(defun rechteckumfang(...))
Wir müssten dann bei der Anwendung der Funktionen auch darauf achten,
dass es nichtzu Verwechslungen kommt. In den objektorientierten Sprachen
geht man einen anderen Weg. Natürlich gibt es auch hier zwei verschiedene
Funktionen, aber beide haben den Namen
umfang. Die eine wird
den Kreis-Objekten zugeordnet, die andere den Rechteck-Objekten. In den
meisten Sprachen ist der Punkt der Operator für die Zuordnung:
KreisObjekt.Umfang() bzw.
RechteckObjekt.Umfang() wären
also die adäquaten Schreibweisen (die Klammern hinter der Methode sind
in C++, Java usw. ein Zeichen dafür, dass es hier um eine Methode und
nicht um Properties geht.
Kreis.Radius würde also ohne Klammern
geschrieben, da es hier um eine gespeicherte Variable geht, nicht um eine
Methode, die etwas berechnet).
In AutoLisp/VisualLisp ist aber der Punkt für die 'dotted pairs' zuständig,
und ausserdem würde ein solcher Operator das ganze Konzept von Lisp
durcheinander bringen - Lisp ist nun mal per se keine objektorientierte
Sprache, und die Lisp-Syntax weicht von Sprachen wie C++ oder Java sowieso
gewaltig ab. Wir müssen in VisualLisp also damit leben, dass es keinen
solchen Operator gibt - die Zusammenhänge müssen auch ohne Operator
deutlich gemacht werden.
Statt
(ein-entity.IntersectWith anderes.entity) müssen wir also
zu
(vlax-invoke-method ...) greifen. Der Name der Methode wird als
Symbol übergeben, und derganze Aufruf sieht nun so aus:
(vlax-invoke-method
(vlax-ename->vla-object ent1)
'IntersectWith
(vlax-ename->vla-object ent2)
acExtendNone
)
Die vordefinierte Konstante
acExtendNone hat den Wert 0 und zeigt
an, dass Schnittpunkte auf 'gedachten' Verlängerungen nicht gewünscht sind.
Wenn es um geschlossene Entities wie Kreise geht, spielt diese Konstante
natürlich keine Rolle.
Schön und gut, das war bisher wirklich nicht aufregend - aber verwerten
können wir das Ergebnis leider noch nicht: Die Rückgabe ist ein Array aus
Doubles - keine Liste! An den Datentyp 'Array' müssen wir uns nun herantasten,
wobei nebenher auch noch zu prüfen ist, ob das Array etwa leer ist, da keine
Schnittpunkte vorhanden sind. Ein
nil können wir von einer Lisp-Funktion
erwarten, nicht aber von ActiveX!
Um an das Array selbst heranzukommen, müssen wir noch einen Aufruf von
(vlax-variant-value ...) drumherum legen:
(vlax-variant-value
(vlax-invoke-method
(vlax-ename->vla-object ent1)
'IntersectWith
(vlax-ename->vla-object ent2)
acExtendNone
)
)
Für den Umgang mit solchen Arrays stellt uns VisualLisp den neuen
Datentyp 'safearray' zur Verfügung - der Name enthält das Wort 'safe',
weil diese Arrays gegen den Zugriff über die Dimensionen des Arrays
hinaus abgesichert sind. Abstürze sind also bei falschen Zugriffen
genauso wenig zu erwarten wie bei Zugriffen hinter das Ende einer
Lisp-Liste. Ein solches 'safearray' kann mit
(vlax-safearray->list)
in eine Lisp-Liste umgewandelt werden.
(vlax-safearray->list
(vlax-variant-value
(vlax-invoke-method
(vlax-ename->vla-object ent1)
'IntersectWith
(vlax-ename->vla-object ent2)
acExtendNone
)
)
)
Noch haben wir das Problem mit dem leeren Array nicht gelöst, und
schon haben wir ein neues Problem am Hals: Die Umwandlung des Arrays
in eine Liste funktioniert zwar irgendwie, gibt uns aber eine lineare
Liste in der Form
(x1 y1 z1 x2 y2 z2 x3 y3 ...) zurück. Die
aus jeweils 3 Koordinaten bestehenden Punkte müssen wir uns also
noch 'zusammenbasteln'. Dazu eine kleine Hilfsfunktion:
(defun gather(ls len / tmp rl i)
(setq i 0)
(foreach item ls
(setq tmp(append tmp(list item)))
(setq i(1+ i))
(if(zerop(rem i len))
(setq rl(cons tmp rl) tmp nil)
)
)
(reverse rl)
)
(gather '(x1 y1 z1 x2 y2 z2 x3 y3 z3 ...))
=> ((x1 y1 z1)(x2 y2 z2)(x3 y3 z3) ...)
Und um zu prüfen, ob wir ein leeres Array zurückbekommen haben, gibt es
die Funktion
(vlax-safearray-get-u-bound ...). Das 'u' steht hier
für 'upper', also das Ende des Arrays. Wenn das Array leer ist, gibt uns
diese Funktion den Wert -1 (Basic lässt grüssen!) zurück. Da die Arrays
bis zu 16-dimensional sein können, muss noch eine 1 für die Dimension
mitgegeben werden (keine Zählung ab 0 wie in den meisten anderen Sprachen,
auch hier lässt Basic grüssen).
Damit haben wir nun alles zusammen, was wir für unsere Funktion
benötigen: Hier die vollständige Implementation (natürlich muss die
Hilfsfunktion
(gather...) auch geladen sein. Der
unvermeidliche Aufruf von
(vl-load-com) muss übrigens sein,
damit alle ActiveX-Funktionen und Konstanten geladen sind und zur
Verfügung stehen.
(defun intersect(ent1 ent2 / ar)
(vl-load-com)
(setq ar
(vlax-invoke-method
(vlax-ename->vla-object ent1)
'IntersectWith
(vlax-ename->vla-object ent2)
acExtendNone
)
)
(if
(/= -1
(vlax-safearray-get-u-bound
(vlax-variant-value ar)
1
)
)
(gather
(vlax-safearray->list
(vlax-variant-value ar)
)
3
)
)
)
Und nun noch eine Testfunktion, mit der man zwei Objekte am
Bildschirm wählen kann. Die Schnittpunkte werden als Punkte
eingezeichnet - man sollte die Systemvariable PDMODE auf
einen entsprechenden Wert setzen, damit das Resultat auch
sichtbar wird. In dieser Testfunktion ist nichts mehr
von ActiveX und
(vlax-...) zu sehen. Wir können die
neue Funktion benutzen wie das altbekannte
(inters ...),
nur kann sie ungleich mehr!
(defun c:intersect( / )
(vl-load-com)
(foreach p
(intersect
(car(entsel "\nErstes Objekt wählen: "))
(car(entsel "\nZweites Objekt wählen: "))
)
(command"_point"p)
)
)