Wie man 2D-Zeichnungen auf einen Zylindermantel aufprägt (Software)

Martin Vogel ⌂ @, Dortmund / Bochum, Donnerstag, 11.04.2019, 22:40 (vor 133 Tagen)

Die DWG-CAD-Programme BricsCAD und AutoCAD besitzen recht eindrucksvolle Modellierungsfähigkeiten. Reichen die eingebauten Befehle nicht aus, lassen sich in der Programmiersprache LISP neue Funktionen dazuschreiben. In den letzten 33 Jahren entstanden so allerorten tausende kleine und große CAD-Hilfswerkzeuge.

Leider ist die uralte Sprache LISP zwar ziemlich leistungsfähig, aber sie ist schon etwas speziell mit ihrer Syntax voller Klammerausdrücke. Ihre fremdartige polnische Notation trägt wohl auch mit dazu bei, dass die meisten CAD-Anwenderinnen und -anwender noch nie eine LISP-Routine selber geschrieben haben.

Ich fragte mich, ob es wohl möglich sei, auch mit einem Python-Programm CAD-Daten aus einer bestehenden Zeichnung zu lesen. Da es weder in AutoCAD noch in BricsCAD eine Programmierschnittstelle für Python gibt, müsste die Datenübergabe allein über Textdateien erfolgen.

Um Daten aus dem CAD-Programm heraus zu bekommen, hilft der Befehl DATENEXTRAKT, der eine CSV-Datei mit den Geometriedaten ausgewählter Zeichnungsobjekte erzeugt. Diese CSV-Datei kann von Python problemlos gelesen werden.

Der Rückweg ist etwas kniffliger, da Python ja wegen der fehlenden Programmierschnittstelle keine Möglichkeit hat, unmittelbar vorhandene Objekteigenschaften abzufragen. Alles, was Python ausgibt, muss praktisch „im Blindflug“ vom CAD-Programm ausgeführt werden. Zu diesem Zweck erzeugt das in diesem Beitrag vorgestellte Pythonprogramm eine SCR-Datei, in der alle Zeichenbefehle genau so stehen, wie sie ohne Maus über die Tastatur eingetippt werden müssten. Sinnvollerweise geschieht das in einer neuen, leeren Zeichnung.

Um beispielsweise das Haus vom Nikolaus bildschirmfüllend darzustellen, würde man folgendes eintippen:

LINIE 1,1 1,3 2,4 3,3 1,3 3,1 1,1 3,3 3,1 ↲
ZOOM G↲


Das Zeichen ↲ steht dabei für das Drücken der Eingabetaste auf der Tastatur. In Python wird das entsprechende Steuerzeichen durch die Ersatzdarstellung \n in die SCR-Datei eingefügt.

Damit unser Pythonprogramm nicht allzu trivial wird, bekommt es die Aufgabe, eine zweidimensionale Zeichnung auf einem Zylindermantel abzubilden. Die Beispielzeichnung unten besteht aus 12 geschlossenen Polylinien. Die Bögen sind als Splines konstruiert worden und verwandeln sich dadurch beim Verbinden mit dem Rest der Polylinie zu einer Kette kurzer Linienstücke. Kreisbogenstücke würden dagegen durch ein einzelnes Linienstück ersetzt.

[image]

Exportiert man die Geometrie aus BricsCAD mittels DATENEXTRAKT in eine CSV-Datei, so kann man auswählen, welche Objektarten und -eigenschaften exportiert werden sollen. Wir beschränken uns bei der Objektart auf „Polylinien-Scheitelpunkte“ und bei den Eigenschaften auf „Position X“, „Position Y“ und „Scheitelpunkt“. Das Häkchen vor „kombiniere gleiche Zeilen“ darf nicht gesetzt sein.

Die „extrahierte“ CSV-Datei besteht aus ein paar hundert Zeilen mit den Koordinaten aller Scheitelpunkte der ausgewählten Polylinien.

Anzahl;Name;Position X;Position Y;Scheitelpunkt
1;Polylinien Scheitelpunkt;8.527978;0.410224;1
1;Polylinien Scheitelpunkt;7.976107;0.410224;2
1;Polylinien Scheitelpunkt;7.976107;1.279974;3
1;Polylinien Scheitelpunkt;7.979059;1.328747;4
1;Polylinien Scheitelpunkt;7.987645;1.377615;5
1;Polylinien Scheitelpunkt;8.001463;1.426065;6
1;Polylinien Scheitelpunkt;8.020111;1.473588;7

Der Zähler in der letzten Spalte ist die Nummer des Scheitelpunkts in der jeweiligen Polylinie. Er beginnt bei jeder neuen Polylinie wieder bei Eins. Wir verwenden ihn, um herauszufinden, zu welcher Polylinie ein Scheitelpunkt gehört.

Unser Programm hat nun die Aufgabe, aus dieser CSV-Datei die X-Y-Koordinaten in 3D-Zylinderkoordinaten umzurechnen und die CAD-Befehle zu erzeugen, die diese Geometrie auf einen Zylindermantel abbilden. Diese werden in eine Datei geschrieben und mit dem Befehl SCRIPT in AutoCAD oder BricsCAD ausgeführt.

Das Ergebnis sieht in unserem Beispiel so aus:

[image]

Mit dem CAD-Befehl AUFPRÄGEN lassen sich auf dem Zylindermantel liegende Flächen erzeugen, die dann beispielsweise verstärkt oder zurückgesetzt werden können. Wenn wir unserem Zylinder eine Wandstärke zuweisen, sieht das Ergebnis der Modellierung so aus:

[image]

Damit die erzeugte Geometrie überhaupt korrekt aufgeprägt werden kann, muss sie exakt auf dem Zylindermantel liegen. Die Verbindungslinien zwischen den Scheitelpunkten sind daher stets Ellipsenbögen, die jeweils auf einer Ebene liegen, welche den Zylinder schneidet. Einzige Ausnahme sind senkrechte Linien, die weiterhin geradlinig bleiben.

Als Kuriosum ist zu beachten, dass weder AutoCAD noch BricsCAD Ellipsenbögen im Raum kennen. Um einen Ellipsenbogen zu zeichnen, muss zuerst eine Zeichenebene durch ein Benutzerkoordinatensystem definiert werden, auf der dann in einem zweiten Schritt ein Ellipsenbogen vom Startpunkt zum Endpunkt gezeichnet werden kann.

[image]

Das Programm kann auch dazu verwendet werden, Schrift auf Zylindermäntel aufzuprägen. Der Text muss lediglich vorher mit dem Befehl TXTAUFL in Polylinien umgewandelt werden. Je nach Font ist nach der Umwandlung ein Zwischenschritt notwendig, in dem möglicherweise vorhandene doppelte Umrisslinien gelöscht werden.

[image]

  1. from math import sin, cos, pi, hypot
  2. from tkinter import Tk
  3. from tkinter.filedialog import askopenfilename
  4. import sys
  5.  
  6. ## Version 5 vom 10. April 2019
  7.  
  8. print("""
  9. Dieses Python-Programm erzeugt ein BricsCAD-Script, um geradlinige Teilstücke
  10. von 2D-Polylinien als Ellipsenbögen auf einem Zylindermantel abzubilden.
  11.  
  12. ### Anleitung ###
  13.  
  14. Schritt 1: Die später auf dem Zylindermantel anzuordnenden Elemente müssen als
  15. 2D-Polylinien in der x-y-Ebene vorliegen. Bei mittels TXTAUFL aus Texten
  16. erzeugten Objekten müssen ggf. doppelte Polylinien gelöscht werden.
  17.  
  18. Schritt 2: In BricsCAD mittels DATENEXTRAKT eine CSV-Datei mit den Scheitel-
  19. punktdaten der gewünschten Polylinien erstellen. Es müssen mindestens die
  20. Eigenschaften "Position X", "Position Y" und "Scheitelpunkt" ausgewählt werden.
  21. Gleiche Elemente dürfen nicht zusammengefasst werden.
  22.  
  23. Als Trennzeichen ist das Semikolon zu wählen.
  24.  
  25. Schritt 3: Diese CSV-Datei laden.
  26. """)
  27.  
  28. Tk().withdraw()
  29. csvname = askopenfilename(title = "CSV-Datei wählen:",
  30. filetypes = (("CSV-Dateien","*.csv"),
  31. ("Alle Dateien","*.*")))
  32. if not csvname:
  33. print("Keine CSV-Datei ausgewählt. Programm wird abgebrochen.")
  34. sys.exit()
  35.  
  36. with open(csvname) as csv:
  37. Zeilen = [Z.strip() for Z in csv]
  38.  
  39. # Kopfzeile der CSV-Datei
  40. Z = Zeilen[0].split(";")
  41.  
  42. # Führt diese alle benötigten Spalten auf?
  43. try:
  44. xSpalte = Z.index("Position X")
  45. ySpalte = Z.index("Position Y")
  46. sSpalte = Z.index("Scheitelpunkt")
  47. except ValueError:
  48. print("""
  49. CSV-Formatfehler!
  50.  
  51. In der Kopfzeile der CSV-Datei müssen die Spaltenüberschriften "Position X",
  52. "Position Y" und "Scheitelpunkt" aufgeführt sein.
  53.  
  54. Programm wird abgebrochen.
  55. """)
  56. sys.exit()
  57.  
  58. # Liste aller Polylinien in der CSV-Datei
  59. PL = []
  60.  
  61. # Grenzen des erkannten Bereichs
  62. xmin = ymin = float("inf")
  63. xmax = ymax = float("-inf")
  64.  
  65. # Alle Zeilen der CSV-Datei auswerten
  66. for Zeile in Zeilen[1:]:
  67. zs = Zeile.split(";")
  68.  
  69. if "Scheitelpunkt;" in Zeile:
  70. # Nummer des Scheitelpunkts
  71. sp = zs[sSpalte]
  72.  
  73. # Beginn einer neuen Polylinie?
  74. if sp == "1":
  75. PL.append([])
  76.  
  77. # x- und y-Ordinaten holen
  78. x = float(zs[xSpalte])
  79. y = float(zs[ySpalte])
  80.  
  81. # Grenzen ggf. erweitern
  82. xmin = min(xmin, x)
  83. ymin = min(ymin, y)
  84. xmax = max(xmax, x)
  85. ymax = max(ymax, y)
  86.  
  87. # Scheitelpunkt an zuletzt begonnene Polylinie anhängen,
  88. # doppelte Punkte überspringen
  89. if not PL[-1] or (x,y) != PL[-1][-1]:
  90. PL[-1].append((x,y))
  91.  
  92. print(len(PL),"Polylinien gefunden.\n")
  93.  
  94. print(f"x-Bereich {xmin:.2f} bis {xmax:.2f}")
  95. print(f"y-Bereich {ymin:.2f} bis {ymax:.2f}")
  96.  
  97. breite = xmax - xmin
  98. höhe = ymax - ymin
  99.  
  100. # Radius für 360° Umwicklung
  101. rmin = breite/pi/2
  102.  
  103. print(f"\nKleinster sinnvoller Radius: {rmin:.2f}")
  104. r = float(input("Gewählter Radius r = "))
  105.  
  106. # Zylinderumfang
  107. u = 2*pi*r
  108.  
  109. def zk(r,x,y):
  110. """Berechnet Zylinderkoordinaten"""
  111. z = y-ymin
  112. w = 2*pi*(x-xmin)/u
  113. x = r*cos(w)
  114. y = r*sin(w)
  115. return x, y, z
  116.  
  117. # Die Scriptdatei heißt wie die CSV-Datei
  118. scrname = csvname[:-4]+".scr"
  119.  
  120. print(f"\nSchreibe Skriptdatei „{scrname}“ …")
  121. with open(scrname,"w") as scr:
  122. # Aus Geschwindigkeitsgründen: visueller Stil "2D-Drahtmodell"
  123. scr.write("-VIS A 2D\n")
  124. # Definierende Geometrie immer löschen
  125. scr.write("DELOBJ 2\n")
  126. # Schrägansicht
  127. scr.write("APUNKT 1,1,1\n")
  128. # Zoom auf interessanten Bereich
  129. scr.write(f"ZOOM F {-r},{-r} {r},{r}\n")
  130.  
  131. # Für alle Polylinien in der Liste:
  132. for P in PL:
  133. # Erster und letzter Punkt müssen übereinstimmen
  134. if P[0] != P[-1]:
  135. P.append(P[0])
  136.  
  137. # Für alle Teilstücke in der Polylinie:
  138. for i in range(len(P)-1):
  139. # Die beiden Endpunkte auf den Zylindermantel projizieren
  140. x1z, y1z ,z1z = zk(r, P[i][0], P[i][1])
  141. x2z, y2z ,z2z = zk(r, P[i+1][0], P[i+1][1])
  142. # Mittlerer Punkt der Linie auf dem Zylindermantel
  143. xmz, ymz, zmz = zk(r,
  144. (P[i][0]+P[i+1][0])/2,
  145. (P[i][1]+P[i+1][1])/2)
  146. # Sehnenlänge im Grundriss
  147. dx = x2z-x1z
  148. dy = y2z-y1z
  149. dh = hypot(dx, dy)
  150. # Höhenunterschied zwischen Start- und Endpunkt
  151. dz = z2z-z1z
  152. # Sehnenlänge im Raum
  153. ds = hypot(dh, dz)
  154.  
  155. # Um Nulldivisionen zu vermeiden, müssen Linien
  156. # parallel zur Zylinderachse gesondert betrachtet werden:
  157.  
  158. if not dh:
  159. # Senkrechte Linie
  160.  
  161. scr.write("LINIE "
  162. f"{x1z},{y1z},{z1z} "
  163. f"{x2z},{y2z},{z2z} \n")
  164.  
  165. else:
  166. # Ellipsenbogen
  167.  
  168. # Radius der langen Halbachse
  169. rl = -ds/dh * r
  170.  
  171. # Radius bis zur Sehnenmitte
  172. rs = hypot((x1z+x2z)/2,(y1z+y2z)/2)
  173.  
  174. # Ellipsenbögen können nur
  175. # im ebenen BKS gezeichnet werden.
  176. scr.write(f"BKS 0,0,{zmz} "
  177. f"{xmz},{ymz},{zmz} "
  178. f"{x2z},{y2z},{z2z}\n")
  179.  
  180. # Wir sammeln alle Punkte ein
  181. scr.write("ELLIPSE B "
  182. f"0,{-rl} "
  183. f"0,{rl} "
  184. f"{r},0 "
  185. f"{rs},{-ds/2} "
  186. f"{rs},{ds/2}\n"
  187. "BKS W\n")
  188.  
  189. # Alle Ellipsen zu "Splines" verbinden
  190. scr.write("VERBINDEN alle \n")
  191.  
  192. # Zum Schluss noch den dazugehörigen Zylinder bauen …
  193. scr.write(f"ZYLINDER 0,0,{-höhe/4} {r} {höhe*1.5}\n")
  194.  
  195. # … und alle Splines auf den Zylindermantel aufprägen
  196. scr.write("AUFPRÄG L ")
  197. for P in PL:
  198. scr.write("alle J ")
  199. scr.write("\n")
  200.  
  201. print("""
  202. Schritt 4: In BricsCAD in einer leeren Zeichnung mittels SCRIPT die gerade
  203. erzeugte Scriptdatei ausführen lassen.
  204.  
  205. Die Umrisse werden vom Skript mittels AUFPRÄGEN auf die Zylinderwand
  206. aufgeprägt.
  207.  
  208. Die dadurch erzeugten Teilflächen der Zylinderwand können mittels
  209. DMVERSTÄRKEN erhaben oder vertieft ausmodelliert werden.
  210.  
  211. """)
  212.  
  213. input("[Enter]")

--
Dipl.-Ing. Martin Vogel
Leiter des Bauforums

Heute schon programmiert? Einführung in Python 3 (PDF)

Tags:
Python, AutoCAD, BricsCAD, Zylinder

RSS-Feed dieser Diskussion
powered by my little forum