Fusion360のAPIでモデリングを自動化しよう！

この記事は[DeNA 24 新卒 Advent Calendar 2023](https://qiita.com/advent-calendar/2023/dena-24-newgrad)の7日目の記事です。

# はじめに
3Dプリンター全盛の時代、3DCADの需要もうなぎ上り。数多ある3DCADソフトの中でも、Autodesk社のFusion360は、モダンなUIとクラウドベースの利便性、お手軽な料金プラン（学生なら無料で使えます！）で人気があります。
そんなFusion360ですが、APIを利用して簡単にスクリプトやアドインが作成できます。言語をPythonとC++から選べる親切仕様です。

この記事では、APIを活用してどのようなことができるのかを紹介しつつ、これからスクリプトを書き始める人の一助となることを目指します。

# 開発前に知っておくべきこと
APIの公式リファレンスはこちら。

https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-A92A4B10-3781-4925-94C6-47DA85A4F65A

内容は充実しているものの、オブジェクトの種類が多いため把握がやや大変です。リンクページ内にある、[オブジェクトモデルの関係図](https://help.autodesk.com/cloudhelp/ENU/Fusion-360-API/ExtraFiles/Fusion.pdf)を参考にするとよいでしょう。

スクリプト開発はエディタ上部の「ユーティリティ」タブ内、「アドイン」ボタンから行います。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/aadb9d14-5345-02f6-ee91-b980ab710f24.png">
本記事ではPythonでコードを記述します。執筆時点ではバージョン3.11.1が組み込まれていました。
スクリプトを選択し、編集ボタンを押すとVSCodeが起動します。VSCodeの強力な機能が使えるのもグッドです。

コードを書き始める前の注意点として、「デザイン履歴をキャプチャ」機能をオフにしておくことをお勧めします。スクリプト実行時にエラーの原因となることがあります。

# サンプルその1：シンプルなモデルを量産しよう
スクリプトの強みの一つが、同じ作業の反復です。適当な大きさの円柱モデルを量産してみます。
~~~python:CylinderFromCSV.py
import adsk.core, adsk.fusion, adsk.cam, traceback
import csv

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

        # 大元のコンポーネントを取得
        design = adsk.fusion.Design.cast(app.activeProduct)
        rootComp = design.rootComponent

        # ファイル指定
        fileDlg = ui.createFileDialog()
        fileDlg.title = 'Fusion Choose File Dialog' 
        fileDlg.isMultiSelectEnabled = False
        fileDlg.filter = '*.csv'
        dlgResult = fileDlg.showOpen()
        if dlgResult == adsk.core.DialogResults.DialogOK:
            filepath = fileDlg.filename
        else:
            return 0
        
        # CSV読み込み
        with open(filepath) as f:
            reader = csv.reader(f)
            data = [row for row in reader]

        # 円柱生成
        extrudes = rootComp.features.extrudeFeatures
        sketches = rootComp.sketches
        for row in data[1:]:
            id, radius, height = row[0], float(row[1]), float(row[2])
            sketch = sketches.add(rootComp.xYConstructionPlane)
            sketchCircles = sketch.sketchCurves.sketchCircles
            centerPoint = adsk.core.Point3D.create(0, 0, 0)
            circle = sketchCircles.addByCenterRadius(centerPoint, radius)
            prof = sketch.profiles.item(0)
            distance = adsk.core.ValueInput.createByReal(height)
            extrude = extrudes.addSimple(prof, distance, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
            body = extrude.bodies.item(0)
            body.name = f'body-{id}'

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
~~~
このスクリプトでは、円柱の半径と高さを記述したCSVファイルを読み込み、3Dモデルを生成します。試しに以下のCSVを読み込ませてみます。
~~~csv
id,radius,height
1,10,50
2,30,20
3,15,8
4,22,30
5,40,15
~~~
<img width="600"src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/e5676d98-b408-ffec-ce53-a4c9e12a088a.png">
ハノイの塔のような図形が現れました。円柱のモデルはそれぞれボディとして独立しているため、「メッシュとして保存」から「ボディごとに1ファイル」を選べば、円柱ごとの3Dモデルがメッシュデータとして保存できます。もちろんメッシュ出力の工程も自動化できます。
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/c750a61c-9904-1467-30dd-0c37bcec7308.png"> <img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/2023aa19-60ef-0470-058b-de9a1a8c66a9.png">
PCの性能にもよりますが、100個単位でも数秒で書き出してくれます。似たような図形を大量に作る場合にお勧めです。

# その2：テキストファイルからはんこを作ってみよう
Fusion360には文字を押し出し・切り抜きできる素敵な機能があります。
![スクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/c84fc02e-a64e-f297-3fcd-beeae52b4a80.png)
この機能を利用して、テキストファイルからはんこの3Dモデルを生成するスクリプトを作ってみます。
~~~python:TextBoard.py
import adsk.core, adsk.fusion, adsk.cam, traceback

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

        # 大元のコンポーネントを取得
        design = adsk.fusion.Design.cast(app.activeProduct)
        rootComp = design.rootComponent

        # ファイル指定
        fileDlg = ui.createFileDialog()
        fileDlg.title = 'Fusion Choose File Dialog' 
        fileDlg.isMultiSelectEnabled = False
        fileDlg.filter = '*.txt'
        dlgResult = fileDlg.showOpen()
        if dlgResult == adsk.core.DialogResults.DialogOK:
            filepath = fileDlg.filename
        else:
            return 0
        
        # テキストファイル読み込み
        with open(filepath, encoding='UTF-8') as f:
            text = f.read()

        # テキスト生成・押し出し
        extrudes = rootComp.features.extrudeFeatures
        sketch = rootComp.sketches.add(rootComp.xYConstructionPlane)
        sketchtexts = sketch.sketchTexts
        input = sketchtexts.createInput2(text,1)
        input.setAsMultiLine(adsk.core.Point3D.create(0, -50, 0),
                             adsk.core.Point3D.create(50, 10, 0),
                             adsk.core.HorizontalAlignments.LeftHorizontalAlignment,
                             adsk.core.VerticalAlignments.TopVerticalAlignment, 0)
        sketchtext = sketchtexts.add(input)
        distance = adsk.core.ValueInput.createByReal(0.3)
        extrude = extrudes.addSimple(sketchtext, distance, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)

        # 土台用の寸法を取得
        maxPoints = []
        minPoints = []
        for body in extrudes.item(0).bodies:
            maxPoints.append(body.boundingBox.maxPoint.asArray())
            minPoints.append(body.boundingBox.minPoint.asArray())
        max_x = max([point[0] for point in maxPoints])
        max_y = max([point[1] for point in maxPoints])
        min_x = min([point[0] for point in minPoints])
        min_y = min([point[1] for point in minPoints])

        # 土台生成
        sketch2 = rootComp.sketches.add(rootComp.xYConstructionPlane)
        pointOne = adsk.core.Point3D.create(min_x, min_y, 0)
        pointTwo = adsk.core.Point3D.create(max_x, max_y, 0)
        rectangles = sketch2.sketchCurves.sketchLines 
        rectangle = rectangles.addTwoPointRectangle(pointOne, pointTwo)
        prof = sketch2.profiles.item(0)
        distance2 = adsk.core.ValueInput.createByReal(-1.0)
        extrude2 = extrudes.addSimple(prof, distance2, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
~~~
ためしに次のテキストファイルを読み込ませてみます。
~~~
今週も
お疲れ様
でした
~~~
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/b2127f64-87ce-ad6b-6a01-b3be32775417.png"> <img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/8cd071dc-371c-ae53-7438-cd942255396e.png">
これをメッシュ化して3Dプリンタで印刷すれば、好きな文章のはんこが作れますね。
ちょっと趣向を変えたデータも入力してみましょう。
~~~
　＼　　　　　　　　　　　　　　　　　　　　/
　　 ＼　　丶　　　　 　 i.　　 |　　　　　 /　　 　 ./　　　　 　 ／
　　　　＼　　ヽ　　　　　i. 　 .|　　　　　/　　　 /　　　　　 ／
　　　　　 ＼　　ヽ　　　　i　　|　　　　 /　　　/　　　　　／
　　　＼
　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　-‐
　　ー
　_＿　　　　 　　　　　　わ　た　し　で　す　　　　　 　　　　　--
　　　　　二　　　　　　　 　　／￣＼　　　　　　　　　　　＝　二
　　￣.　　　　　　　　　　　　|　＾o＾　|　　　　　　　　　　　　　　　　￣
　　　　-‐　　　　　　　　　　 ＼＿／ 　　　　　　　　　　　　　　　‐-

　　　　／
　　　　　　　　　　　　/　　　　　　　　　　　　　　　ヽ　　　　　 ＼
　　　　／　　　　　　　　　　　　　　　　　　　　丶　　　　 ＼
　　 ／　　　/　　　 /　　　　　　|　　　i,　　　 　 丶　　　　　＼
　／　　　 /　　　　/　　　　　　 |　　　 i,　　　　　　丶　　　　　＼
~~~
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/475c34f5-4a4f-10b9-3c9f-c5ff9a8cfd2f.png"> <img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192029/ec05886d-9293-5004-7b1d-a68609447c48.png">
シンプルなAAならはんこ化できそうです！

# 終わりに
以上、簡単なサンプルプログラムを二つ紹介しました。これから開発を始める人の助けになれば幸いです。
ネット上にあるFusion360関連の記事のほとんどはモデリングについてで、APIについて書かれた記事はあまり多くないのが現状です。実用的かつ遊べる魅力的なツールなので、解説記事が今後増えていくことを期待しています。