DXFと亀

#この記事の概要
pythonでDXFファイルをざっくりと読み込んで内容を表示させてみた。Windows10 32bit Anaconda 環境で試しましたが、pythonの動く環境ならほぼ同じくできると思います。

#DXFとは
**D**ata  e **X**change **F** ormat の略である(…と、思っていたのですが[違っていたのかもしれません](http://qiita.com/kazhida/items/8a5fa68e8fa135614d5d))。JSONやXMLのライバルか親戚みたいな名前だが、別にそんなものではなく異なるCAD間での**図面データ**をやりとりするための中間・共通フォーマットみたいなもんである。インターネットもろくに発達していない時代から存在し、各社が独自の解釈や拡張をしつつ、規格自体もころころ変化・拡張されてきたため、とっつきづらいものになっている。しばしば他のCADで吐き出したDXFデータが読み込めないことがあるし、CADソフトでDXFの読み書きに関する設定を開くと**けっこう膨大であまり見たくはない**。自分が普段使っている分には直線と円と円弧の2Dデータが扱えればよい程度なのだが(3DデータはSAT,IGES,STEPなどでやりとりが多い)、DXFの規格としては自由曲線、自由曲面、メッシュ、ソリッドなども扱えるよう拡張されているようである。

#で、それがなに？
個人的につくろうとしているアプリケーションがあり、必須ではないがちょいとした作図・製図機能があれば応用が拡がりそうだ。が、そんな作図機能の実装は大変すぎる。じゃあ他のCADソフトでつくった図形をインポートするのはどうよ？調べたらpythonに**dxfgrabber**というモジュールがあるのでいっちょ試してみっか？…というのがこの記事です(前置き長すぎ)。他にpycamなんていうソフトもあってdxfの読み書きもできるはずなのでソースを少し眺めてみましたが、どこで何やってるかパッと見てわかるような感じではなかったのでpycamハック路線は放置中。

#dxfgrabber
pypiでキーワードDXFで検索すると上のほうに出てきます。コンパイルとかバイナリとか関係なさそうですし、Anaconda環境にpipでインストールしても何の問題もなさそうです。

```
pip install dxfgrabber
```

##簡単な使い方
###ファイル読み込み

```
>>> import dxfgrabber
>>> dxf=dxfgrabber.readfile("hogehoge.dxf")
```

###各種ヘッダの表示
上記読み込みで各種ヘッダが辞書に格納される。DXFを吐き出したCADソフトによって数項目しかなかったりびっちり項目が出てきたりするあたりにDXFの闇を感じる。

```
>>> print(dxf.header)
{'$ACADVER': 'AC1015', '$DWGCODEPAGE': 'ANSI_932', '$ACADMAINTVER': 6, '$INSBASE': (0.0, 0.0, 0.0), '$EXTMIN': (-250.0, -250.0, 0.0), '$EXTMAX': (250.0, 250.0, 0.0), '$LIMMIN': (-250.0, -250.0), '$LIMMAX': (250.0, 250.0), '$ORTHOMODE': 0, '$REGENMODE': 1, '$FILLMODE': 1, '$QTEXTMODE': 0, '$MIRRTEXT': 1, '$LTSCALE': 1.0, '$ATTMODE': 1, '$TEXTSIZE': 0.2, '$TRACEWID': 0.05, '$TEXTSTYLE': 'Standard', '$CLAYER': '0', '$CELTYPE': 'ByLayer', '$CECOLOR': 256, '$CELTSCALE': 1.0, '$DISPSILH': 0, '$DIMSCALE': 1.0, '$DIMASZ': 0.18, '$DIMEXO': 0.0625, '$DIMDLI': 0.38, '$DIMRND': 0.0, '$DIMDLE': 0.0, '$DIMEXE': 0.18, '$DIMTP': 0.0, '$DIMTM': 0.0, '$DIMTXT': 0.18, '$DIMCEN': 0.09, '$DIMTSZ': 0.0, '$DIMTOL': 0, '$DIMLIM': 0, '$DIMTIH': 1, '$DIMTOH': 1, '$DIMSE1': 0, '$DIMSE2': 0, '$DIMTAD': 0, '$DIMZIN': 0, '$DIMBLK': '', '$DIMASO': 1, '$DIMSHO': 1, '$DIMPOST': '', '$DIMAPOST': '', '$DIMALT': 0, '$DIMALTD': 2, '$DIMALTF': 25.4, '$DIMLFAC': 1.0, '$DIMTOFL': 0, '$DIMTVP': 0.0, '$DIMTIX': 0, '$DIMSOXD': 0, '$DIMSAH': 0, '$DIMBLK1': '', '$DIMBLK2': '', '$DIMSTYLE': 'Standard', '$DIMCLRD': 0, '$DIMCLRE': 0, '$DIMCLRT': 0, '$DIMTFAC': 1.0, '$DIMGAP': 0.09, '$DIMJUST': 0, '$DIMSD1': 0, '$DIMSD2': 0, '$DIMTOLJ': 1, '$DIMTZIN': 0, '$DIMALTZ': 0, '$DIMALTTZ': 0, '$DIMUPT': 0, '$DIMDEC': 4, '$DIMTDEC': 4, '$DIMALTU': 2, '$DIMALTTD': 2, '$DIMTXSTY': 'Standard', '$DIMAUNIT': 0, '$DIMADEC': 0, '$DIMALTRND': 0.0, '$DIMAZIN': 0, '$DIMDSEP': 46, '$DIMATFIT': 3, '$DIMFRAC': 0, '$DIMLDRBLK': '', '$DIMLUNIT': 2, '$DIMLWD': -2, '$DIMLWE': -2, '$DIMTMOVE': 0, '$LUNITS': 2, '$LUPREC': 4, '$SKETCHINC': 0.1, '$FILLETRAD': 0.5, '$AUNITS': 0, '$AUPREC': 0, '$MENU': '.', '$ELEVATION': 0.0, '$PELEVATION': 0.0, '$THICKNESS': 0.0, '$LIMCHECK': 0, '$CHAMFERA': 0.0, '$CHAMFERB': 0.0, '$CHAMFERC': 0.0, '$CHAMFERD': 0.0, '$SKPOLY': 0, '$TDCREATE': 2453737.5, '$TDUCREATE': 2453737.125, '$TDUPDATE': 2455188.75, '$TDUUPDATE': 2455188.375, '$TDINDWG': 1.16e-08, '$TDUSRTIMER': 1.16e-08, '$USRTIMER': 1, '$ANGBASE': 0.0, '$ANGDIR': 0, '$PDMODE': 0, '$PDSIZE': 0.0, '$PLINEWID': 0.0, '$SPLFRAME': 0, '$SPLINETYPE': 6, '$SPLINESEGS': 8, '$HANDSEED': '220', '$SURFTAB1': 6, '$SURFTAB2': 6, '$SURFTYPE': 6, '$SURFU': 6, '$SURFV': 6, '$UCSBASE': '', '$UCSNAME': '', '$UCSORG': (0.0, 0.0, 0.0), '$UCSXDIR': (1.0, 0.0, 0.0), '$UCSYDIR': (0.0, 1.0, 0.0), '$UCSORTHOREF': '', '$UCSORTHOVIEW': 0, '$UCSORGTOP': (0.0, 0.0, 0.0), '$UCSORGBOTTOM': (0.0, 0.0, 0.0), '$UCSORGLEFT': (0.0, 0.0, 0.0), '$UCSORGRIGHT': (0.0, 0.0, 0.0), '$UCSORGFRONT': (0.0, 0.0, 0.0), '$UCSORGBACK': (0.0, 0.0, 0.0), '$PUCSBASE': '', '$PUCSNAME': '', '$PUCSORG': (0.0, 0.0, 0.0), '$PUCSXDIR': (1.0, 0.0, 0.0), '$PUCSYDIR': (0.0, 1.0, 0.0), '$PUCSORTHOREF': '', '$PUCSORTHOVIEW': 0, '$PUCSORGTOP': (0.0, 0.0, 0.0), '$PUCSORGBOTTOM': (0.0, 0.0, 0.0), '$PUCSORGLEFT': (0.0, 0.0, 0.0), '$PUCSORGRIGHT': (0.0, 0.0, 0.0), '$PUCSORGFRONT': (0.0, 0.0, 0.0), '$PUCSORGBACK': (0.0, 0.0, 0.0), '$USERI1': 0, '$USERI2': 0, '$USERI3': 0, '$USERI4': 0, '$USERI5': 0, '$USERR1': 0.0, '$USERR2': 0.0, '$USERR3': 0.0, '$USERR4': 0.0, '$USERR5': 0.0, '$WORLDVIEW': 1, '$SHADEDGE': 3, '$SHADEDIF': 70, '$TILEMODE': 1, '$MAXACTVP': 64, '$PINSBASE': (0.0, 0.0, 0.0), '$PLIMCHECK': 0, '$PEXTMIN': (1e+20, 1e+20, 1e+20), '$PEXTMAX': (-1e+20, -1e+20, -1e+20), '$PLIMMIN': (0.0, 0.0), '$PLIMMAX': (12.0, 9.0), '$UNITMODE': 0, '$VISRETAIN': 1, '$PLINEGEN': 0, '$PSLTSCALE': 1, '$TREEDEPTH': 3020, '$CMLSTYLE': 'Standard', '$CMLJUST': 0, '$CMLSCALE': 1.0, '$PROXYGRAPHICS': 1, '$MEASUREMENT': 1, '$CELWEIGHT': -1, '$ENDCAPS': 0, '$JOINSTYLE': 0, '$LWDISPLAY': 0, '$INSUNITS': 4, '$HYPERLINKBASE': '', '$STYLESHEET': '', '$XEDIT': 1, '$CEPSNTYPE': 0, '$PSTYLEMODE': 1, '$FINGERPRINTGUID': '{3D9DC12F-B372-4948-9689-D346C0BFA940}', '$VERSIONGUID': '{FAEB1C32-E019-11D5-929B-00C0DF256EC4}', '$EXTNAMES': 1, '$PSVPSCALE': 0.0, '$OLESTARTUP': 0}

>>> dxf.header["$EXTMIN"]
(-250.0, -250.0, 0.0)

>>> dxf.header["$EXTMAX"]
(250.0, 250.0, 0.0)
```

各種headerの意味とか分からないしあまり分かりたくもないが、"\$EXTMIN", "$EXTMAX"キーに作図されたデータの最小座標、最大座標が入っているようだ。

###直線・円・円弧の取り出し例
```
all_lines = [entity for entity in dxf.entities if entity.dxftype == 'LINE']
all_cirs = [entity for entity in dxf.entities if entity.dxftype == 'CIRCLE']
all_arcs = [entity for entity in dxf.entities if entity.dxftype == 'ARC']
```
ドキュメントによるとentitiesプロパティは"list like collection"とのことでリストとは何かが少し違うようだが、詳しくは不明。上記のようにすれば all_lines[0],all_lines[1],...のようにして値を取り出せるようになる。さらに下記のようにしてより詳しい図形情報が取り出せる。

```
all_lines[i].start[0] # (i+1)番目の直線の開始X座標
all_lines[i].start[1] # (i+1)番目の直線の開始Y座標
all_lines[i].end[0]   # (i+1)番目の直線の終端X座標
all_lines[i].end[1]   # (i+1)番目の直線の終端Y座標

all_arcs[i].start_angle # (i+1)番目の円弧の開始角度[deg]
all_arcs[i].end_angle   # (i+1)番目の円弧の終了角度[deg]
all_arcs[i].radius      # (i+1)番目の円弧の半径
all_arcs[i].center[0]   # (i+1)番目の円弧の中心X座標
all_arcs[i].center[1]   # (i+1)番目の円弧の中心Y座標

all_cirs[i].radius    # (i+1)番目の円の半径
all_cirs[i].center[0] # (i+1)番目の円の中心X座標
all_cirs[i].center[1] # (i+1)番目の円の中心Y座標
```
※色や線種(実線・点線・etc)や太さなどは自分の今回の用途には関係ないので調べてないですごめんちゃい。

#dxfgrabberで図形情報を取り出せたみたいだけど…描画して確かめてみよう

dxfgrabberでdxfは正しく読み込めているのか確認の描画をしてみる。OpenCVなど、てきとーなライブラリやGUIフレームワークでなんちゃらDrawline()とかほげほげDrawArc()を使えばいいのだろうが、ちょっとした事情と思いつきでpythonの**turtleモジュール**というやつを使ってみることにした。ドキュメントは意外と量があり「あれ？思ったよりめんどくさい？」と思わされたが使ってみればやっぱり簡単にできた。円や円弧の動きが少し分かりにくかったが、

```
turtle.circle(r,extent=deg)
```

としたときの動きは下図のようになった。deg<0のときは後退しながら弧を描く。

![turtle_dir.png](https://qiita-image-store.s3.amazonaws.com/0/189966/dd02f318-5188-5168-247b-6d80a139b98e.png)

##DXFファイルを読み込んでturtleで描画するコード
```dxf_turtle.py

# -*- coding: utf-8 -*-

import sys
import dxfgrabber as dg
import turtle as tut

from numpy import pi,cos,sin,arctan2,sqrt

fname=sys.argv[1]
dxf=dg.readfile(fname)
print(dxf.header)

def tut_dxfline(l):
    x0=l.start[0]
    y0=l.start[1]
    x1=l.end[0]
    y1=l.end[1]
    deg = arctan2(y1-y0,x1-x0) / pi*180
    dist = sqrt((x1-x0)**2+(y1-y0)**2)
    
    tut.penup()
    tut.setpos(x0,y0)
    tut.setheading(deg)
    tut.pendown()
    tut.forward(dist)
    
def tut_dxfarc(a):
    deg0=a.start_angle
    deg1=a.end_angle
    r=a.radius
    x0=a.center[0]
    y0=a.center[1]
    if deg0 > deg1:
        deg0 = deg0-360

    tut.penup()
    tut.setpos(x0+r*cos(deg0/180.*pi),y0+r*sin(deg0/180.*pi))
    tut.setheading(deg0+90)
    tut.pendown()
    tut.circle(r,extent=(deg1-deg0))

def tut_dxfcir(c):
    r=c.radius
    x0=c.center[0]
    y0=c.center[1]

    tut.penup()
    tut.setpos(x0+r,y0)
    tut.setheading(90)
    tut.pendown()
    tut.circle(r)

#tut.speed("fastest")
tut.speed("fast")

for entity in dxf.entities:
    if entity.dxftype == 'LINE':
        tut_dxfline(entity)
    elif entity.dxftype == 'CIRCLE':
        tut_dxfcir(entity)
    elif entity.dxftype == 'ARC':
        tut_dxfarc(entity)
    else:
        print(entity.dxftype)

print("Draw End")            
tut.mainloop()
```
上記コードをコマンドラインから

```
 python dxf_turtle.py hogehoge.dxf
```
などと実行すれば色・スケール・線種などは無視しているが、元の図形と同じ図形が描画された。試しに描いた図形の結果をいちおう載せておきますが地味ですね。結果だけ見ると地味地味ですが動いて描画される様は見ていて意外と楽しい。コードに関しては亀の開始位置や向きのセットやpenup()/pendown()などの余計なコードがあるせいでopenCVなどでサッと描画するのより長くなっている気もしますが、動く様が意外と楽しいのでよしとします(大事なことなので2度なんちゃらかんちゃら)。

![hw-k.png](https://qiita-image-store.s3.amazonaws.com/0/189966/110e7738-1d9c-0580-5819-e7c776a191a4.png)

![wheelhub.png](https://qiita-image-store.s3.amazonaws.com/0/189966/0a96c7a7-6071-4a4e-2322-a16c4a151039.png)

#バグ？
色々なDXFデータをturtleで描かせてみたら一部の円弧でおかしくなるものがありました。なぜかY軸線対称に位置が反転しています。プロパティcenter[0]の正負が逆転して読み込まれるようです。複数のCADで読み込ませても起きない現象なのでdxfgrabberのバグなのかな？と思います。そうは言ってもdxfgrabberとその作者には賛嘆です。めんどくさそうなことを簡単にしてくれてありがとう。

#余談：謎の亀推し
ここで使っているturtleモジュールとかROSのturtlebotだとか、コンピュータやロボットの界隈で謎の亀推しがあるような気がするのだけど、その源流がpythonのturtleモジュールの元になってるLOGO言語とかいうのなのだろうか？

こんなのも見つけた[beachbot](http://www.beachbot.ch/)([日本語でとりあげてるサイトのひとつ](http://makezine.jp/blog/2015/10/disney-turtle-robot-draws.html))。サイトの解説見る限りROSもLOGOも関係なくたまたま海辺の生き物として亀が採用されただけかもしんないけど…。