Fusion360 python拡張

#はじめに

個人でものづくりをしたくFusion360というCADを使おうとしています。色々なCADやCGアプリケーションをチラ見してきたのですが、pythonやなんらかのプログラム言語で機能拡張できるものは多いです。

- FREECAD
- Rhinoceras
- Blender
- MAYA
- Inventor(VBA)

と言っても拡張言語だけ分かっていれば色々できるわけではなく、言語とアプリケーション間のインターフェースはアプリケーション毎にまったく異なっており、それぞれ学習する必要があります。しかし製造元が同じならばなんらかの共通性はあるかもしれません(Autodesk製のFusion360,MAYA,Inventorなど)。この投稿ではFusion360用のpythonでサンプル的なプログラムを作って動かしてみます。なおこの投稿ではこれ以上触れませんがFusion360では以下の言語も使えます。

- C++
- Javascript
- Dynamo

ただしJavascriptは実行速度が遅くユーザも少なかったためか2017夏頃？に新規作成ができなくなり**サポート終了気味**です。既存のスクリプトを走らせることはできるようです。Dynamoは機能ブロックを配置してブロック間の入出力をグラフで繋ぐようなビジュアルプログラミング環境です。

#拡張機能の用途例

CADの機能をプログラム言語で拡張するって例えばどんなことなんでしょうか？こんなことのようです。

##元来的なもの
- 通常操作では作れない・作りづらい線・面・ソリッドを描く
    - 小難しい数式に基づいた線や面
    - 別に難しい数式とかではなくても単純な分岐や繰り返しが非常に多くて手作業では頭おかしくなりそうな作図・モデリング。例えばフラクタル図形。
- 独自orマイナーなファイルフォーマットのインポートを実現する(自作の実験機器や実験プログラムの出力ファイルを取り込むなど)

##python(等)が使えるなら…こういうのも
豊富なライブラリでいろいろやりたくなります…よね？

- **ネットワーク接続してなにかする**(Autodesk社がFusion360の拡張言語としてpythonを採用したのは元々こういう狙いがあったようです)
- **opencv** で画像やカメラから検出したエッジを取り込んだり(この程度は代替がすでにありそうな気はする)、ステレオカメラ等から3D点群を取り込んだりする。
- 機械学習ライブラリを活用できて超頑張れば**ジェネレーティブデザイン**の真似事くらいはできるかも?(ハードルは超高そう)

しかし、いずれ別の投稿として書きたいですが、**Fusion360用pythonのライブラリ追加は容易ではなく、やり方もこれがベストというのがないっぽい**です。複雑・高度なことがやりたい場合、Fusion360用のpythonを拡張するのではなく、フル機能の別プログラムと通信するのも検討したほうがいいかもしれません。

#初歩操作…は省略しますが補足事項

「Fusion360 python api」などでググると、いろいろ解説サイトがありますので、スクリプトの新規作成・編集、あらかじめ入っているサンプルスクリプトの起動の仕方などはこの投稿では省略します。

- 2018/09時点でFusion360用pythonのバージョンは3.5です。
- **スクリプト内の数値の単位はcmです。**と言うか、設定単位の10倍みたいです。設定単位がインチならスクリプト内の長さ1はウィンドウ内では10インチになりますし、設定単位がmならばスクリプト内の長さ1はウィンドウ内では10mになります。**かなり面食らいますがこういうもののようです。**
- スクリプトやpython環境はローカルに保存されます。Fusion360ではCADデータはデフォルトでクラウドに保存されるのでそれと同じ感覚でいて複数マシンで作業していると「書いたスクリプトがなくなってる？」ってなるかも。
- print()でspyder内のコンソールに表示について。逐次表示はされず、スクリプト終了時にまとまって表示されます。どうなってるのか仕組みは私にはよく分かりません。
- **無限ループするようなスクリプトを実行するとFusion360がフリーズしたようになり強制終了するしかないみたいです。**何かスクリプトだけを止める方法があるのかもしれませんがちょっと分かりませんでした。無限ループに気をつけましょう。
- エラーの起きるであろうスクリプトを実行すると、エラーの内容を示すダイアログが出て中断されるときと何も表示されず何も起きていないように見えるときがある。スケルトンコードに従ってtry-exeptで例外をキャッチするとダイアログが出て中断されるが、そこにたどり着く前にこけてしまうと何のエラー表示もされないようです。spyderからの実行ならばどんな場合でもエラーの内容は表示してくれるはずですが、spyder内の実行でもエラーが起きて中断されているっぽいが何も表示されず何も起きていないように見えたことがあった気がしますが、このへんは検証しきれていません。


#拡張スクリプト例

とりあえずpythonの追加ライブラリのインストールしなくてもできるものです。

##tkinter?
色々な解説サイトで紹介されているあらかじめ入っているSpurGearというサンプルスクリプトを起動してみますと、

![q-tkinter1.png](https://qiita-image-store.s3.amazonaws.com/0/189966/be6de1ee-df19-859d-c262-c8a5f9d7ee1b.png)

こんなGUIが出てきてパラメータを入れてOK押すとソリッドギヤができておーすごい、となったのですが、このGUIパーツはFusion360用APIで書かれているようなのですが、**できればそんなの覚えたくない、**というわけでpythonの標準GUIであるtkinterが使えるか試してみました。

```tkinter_test.py

import adsk.core, adsk.fusion, adsk.cam, traceback

import tkinter as tk

radius=None

def chg_radius(scale_val):
    global radius
    radius=scale_val

root = tk.Tk()
root.title("Fusion360+tkinter sample")
root.minsize(400,50)
rad_scale=tk.Scale(root, label="radius", orient='h', from_=1,to=30, command=chg_radius)
rad_scale.set(15)
rad_scale.pack(fill=tk.X)

def run(context):
    root.mainloop()
    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        
        design = app.activeProduct

        # Get the root component of the active design.
        rootComp = design.rootComponent

        # Create a new sketch on the xy plane.
        sketches = rootComp.sketches
        xyPlane = rootComp.xYConstructionPlane
        sketch = sketches.add(xyPlane)

        # Draw some circles.
        circles = sketch.sketchCurves.sketchCircles
        circle1 = circles.addByCenterRadius(adsk.core.Point3D.create(0, 0, 0), float(radius))

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

```
実行すると

![q-tkinter2.png](https://qiita-image-store.s3.amazonaws.com/0/189966/257c049b-c260-6982-aae3-fb23d0f6ec26.png)

こんなスライダGUIが出てきてそれを適当にずらしてから閉じるとスライダの値に応じた径の円が描かれます(上の画像では1回スクリプト実行したので既に円が描いてあります)。というわけで、どうやらtkinterによるGUIでも書けるようです。tkinter以外にもpython用GUIはいろいろありますがそれらは追加インストールが必要、しかしFusion360用pythonの環境への追加はちょっと面倒です。**不人気なtkinterですが標準装備であることにはそれなりの強みがありますね。**

しかし、個人的なモデリング用途ならGUIなど作らなくても内部の変数を直に書き換えればよいので…あまりこんなことを考える必要はなかったかも。GUIは他の人に使ってもらうようなものを作るときに意識すべきもの…かも。

##径とピッチが変化する螺旋

通常操作で簡単に描けなさそうな図形の例として径とピッチが変化する螺旋(を近似した連続微小直線)を描いてみます。
「径とピッチが変化する螺旋」…例として挙げておきながらも実用性はないよね…と思っていましたが、ネットをさまよっていたら、スクリュー製造を得意とする鉄工所さんがそういう依頼を受けて製作した事例紹介もあったりしました([このリンク](http://s-iw.com/12-screw.html)の2015/06/19)。螺旋は奥が深い。

```spiral-test.py

import adsk.core, adsk.fusion, traceback

from math import pi,sin,cos

START_PHASE=0
END_PHASE=pi*48
DPHASE=pi/10

def spr_radius(phase):
    aaa=phase/pi*5
    if aaa>=24*5:
        return 24*5
    else:
        return aaa
    
def spr_len(phase):
    return pow(phase/pi,2)

def run(context):
    ui = None
    try: 
        app = adsk.core.Application.get()
        ui = app.userInterface

#        doc = app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
        design = app.activeProduct

        # Get the root component of the active design.
        rootComp = design.rootComponent

        # Create a new sketch on the xy plane.
        sketches = rootComp.sketches;
        xyPlane = rootComp.xYConstructionPlane
        sketch = sketches.add(xyPlane)

        lines = sketch.sketchCurves.sketchLines;

        phase=START_PHASE
        pre_endp=None
        while True:
            R=spr_radius(phase)
            z=spr_len(phase)
            x=R*cos(phase)
            y=R*sin(phase)
            endp=adsk.core.Point3D.create(x, y, z)
            if phase!=START_PHASE:
                lines.addByTwoPoints(pre_endp, endp)
            pre_endp=endp
            phase+=DPHASE
            if phase>END_PHASE:
                break

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

```

上記例の結果は下のようになります。端点は非表示にしてあります(**Fusion360 endpoint hide** 等でググると端点非表示のやり方出てきます)。

![q-sp0.png](https://qiita-image-store.s3.amazonaws.com/0/189966/614e4ad6-bb71-c834-9e31-3473dac63923.png)

描けるのですが、~~プロファイル~~ パス(連続図形)にはならないようです。螺旋に沿ったパイプでも作ろうとしたら微小直線を1個ずつ選ばないとできませんし、スイープのパスとしても微小直線1本しか選べません。Fusion360の基本が分かっていないのかAPIの使い方が合ってないのか、それともこういうものなのか今の私には分かりません。

(追記：3D点群を通るスプラインを描けるAPIがあるようなのでそれを使えばいいかもしれません)

###ちなみにふつうの螺旋
ちなみにFusion360で径もピッチも一定な基本的なスクリュー形状の描き方についてですが、ネット上では「回転させながらスイープはできない？ので円柱からばねのような形状を引き算してスクリューにする」みたいな方法をよく見ます。が、モデル作業ではなく**パッチ作業では回転させながらスイープはできるようです。**

![q-sp1.png](https://qiita-image-store.s3.amazonaws.com/0/189966/ee9221a6-c4d1-de64-c354-c5313b185098.png)

あらかじめ描いたパスの長さとダイアログ内のねじり角でピッチが決まります。できた面に厚みをつけて軸を足せば

![q-sp2.png](https://qiita-image-store.s3.amazonaws.com/0/189966/92fba18b-8890-b0cc-d142-24bd8efd1aa6.png)

こんなふうになります。この例ではプロファイルは直線1本ですが断面形状を描いて選ぶこともできます。**板厚一定**や断面形状を意識したスクリューを描きたいのなら円柱からばねを引き算するようなやり方よりもこの方法がよさそうです。

##ダウンロード&整形
インターネット経由でデータをダウンロードし、それに基づいて描画する例を挙げます。

[国土地理院ベクトルタイル提供実験](https://github.com/gsi-cyberjapan/vector-tile-experiment)　から道路の中心線の緯度経度データをダウンロードして描画してみます。geojsonというデータ形式になっていることやダウンロードするさいに経度緯度に基づいたX,Yの数値を指定する必要があるのですが、それらの解説はここでは省かせていただきます。


```dl_geojson_test.py

import adsk.core, adsk.fusion, adsk.cam, traceback

import requests

from pprint import pprint
from time import time

def run(context):
    ui = None
    try:
        
        app = adsk.core.Application.get()
        ui  = app.userInterface
        design = app.activeProduct

        rootComp = design.rootComponent
        sketches = rootComp.sketches
        xyPlane = rootComp.xYConstructionPlane
        sketch = sketches.add(xyPlane)

        lines = sketch.sketchCurves.sketchLines;

        urls=["https://cyberjapandata.gsi.go.jp/xyz/experimental_rdcl/16/58062/25366.geojson",
              "https://cyberjapandata.gsi.go.jp/xyz/experimental_rdcl/16/58062/25367.geojson"]

#       市町村境界を描いてみようとしましたが、要素が多すぎるのか?うまくいきません
#        urls.append("https://raw.githubusercontent.com/niiyz/JapanCityGeoJson/master/geojson/15/15204.json")

        t1=time()
        for url in urls:
            geo_dat = requests.get(url).json() #この1行でダウンロードとjson形式を辞書形式に変換している
#            pprint(geo_dat.values())
            for gd in geo_dat["features"]:
                ft_type=gd["geometry"]["type"] #LineString, Polygon に対応
                one_path=gd["geometry"]["coordinates"]
    
                if ft_type=="Polygon":                
                    x0=one_path[0][0][0]
                    y0=one_path[0][0][1]
                    n=range(len(one_path[0])-1)
                else:
                    x0=one_path[0][0]
                    y0=one_path[0][1]
                    n=range(len(one_path)-1)    
                    
                pre_endp=adsk.core.Point3D.create(x0, y0, 0)
    
                for i in n:
                    if ft_type=="Polygon":                
                        x=one_path[0][i+1][0]
                        y=one_path[0][i+1][1]
                        # one_path[1]以降に穴/島の図形データが入っていることもあり得るが今回は無視
                    else:
                        x=one_path[i+1][0]
                        y=one_path[i+1][1]
                    endp=adsk.core.Point3D.create(x, y, 0)
                    lines.addByTwoPoints(pre_endp, endp)
                    pre_endp=endp
                
        t2=time()
        print("process time:",t2-t1)

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

```

実行すると

![q-dljson2.png](https://qiita-image-store.s3.amazonaws.com/0/189966/c99fb80f-3016-cd0e-442d-71cd39b4aa7f.png)

このようになります。意外と時間がかかり、数十秒～1分かかります。これは某所の道路なのですが、該当地域をGooglemapで確認するとおおむね以下のようになり、合致しています。

![q-dljson1.jpg](https://qiita-image-store.s3.amazonaws.com/0/189966/10a30a1b-6c9e-baee-3590-eec08f107f1f.jpeg)


なお、有志の方による[JapanCityGeoJson 2016](https://github.com/niiyz/JapanCityGeoJson)という市町村境界のデータの描画も試みたのですが、要素が多いとなぜかうまくいかず原因もよく分からないので今回は諦めました。上の例では描画されている線分の数は約400本で描画は数十秒～1分で、線分の数が2000本のデータを描画すると5分くらい、線分3000本だと20分くらい、そして6000本だと1時間半ほど待っても反応がないので諦めて中断しました。

また、この例では経度をx,緯度をyとしてそのまま描画していますが、それだと北のほうが拡大されて歪みます。**pyproj**というライブラリで緯度経度を直行座標系に近似変換して歪みをなくすことができ、またそのことによって周長や面積も正確に出せたりするのですが、ライブラリのインストールが上手くいかず断念しました。しかし、南北歪みを無くすことと周長の正確さに関しては、せっかくの3DCADなのですから3D空間の球面や楕円体上にだいたい沿わせて描くのもよいかもしれません(この方法で面積が出せるかは…不明)。


#終わり
以上、簡単な描画例の紹介ばかりでした。本来はもっと色々できるようですが、今回はここまで。