Fusion360でジョイントのアニメーションを簡単に作りたい

[C#ATIA]:https://kantoku.hatenablog.com/
[FusionDoc]:https://help.autodesk.com/view/fusion360/ENU

## はじめに
Fusion360でジョイントのアニメーションを作成したい時ってありますよね？

Fusion360に搭載されているAnimationモードはせっかく設定したジョイントを認識してくれませんしお手軽とは言えないので、FusionのAPIを使ってお手軽にgifが作れないか試してみました。

具体的にはViewPortクラスとTextCommandというFusion360の機能を使用しました。

## 作成できるgifの例
以下のようなgif動画がお手軽に作成できます。

![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781638/09b3987a-a270-5c91-4496-c5b71cd61a3d.gif)

## ソースコード
```python:RecordJointMotions.py
import adsk.core, adsk.fusion, traceback
import os, sys, glob

def run(context):
    app = adsk.core.Application.get()
    ui  = app.userInterface
    pyDir = os.getcwd() + "\\Python\\"

    try:
        # try to import PIL
        from PIL import Image
    except:
        if ui:
            ui.messageBox('Failed to import PIL!')
            ui.messageBox(
                'Please install Pillow by the following command:\n'
                f'{pyDir}python.exe -m pip install Pillow'
            )
            return
    try:
        des = adsk.fusion.Design.cast(app.activeProduct)

        # get the output folder.
        fd = ui.createFolderDialog()
        fd.title = "Specify Output Folder"
        if fd.showDialog() != adsk.core.DialogResults.DialogOK:
            return
        resultFolder = fd.folder

        # start animation.
        app.executeTextCommand(u"Commands.Start AnimateModelCmd")

        # capture screenshot multiple times.
        loopCnt = 50
        cnt = 0
        for i in range(loopCnt):
            adsk.doEvents()
            filename = os.path.join(resultFolder, "frame" + str(cnt).zfill(4))
            app.activeViewport.saveAsImageFile(filename, 0, 0)
            cnt += 1

        # end animation.
        app.executeTextCommand(u"NuCommands.CommitCmd")

        # Save as a GIF
        frames = [os.path.join(resultFolder, "frame" + str(i).zfill(4) + ".png") for i in range(loopCnt)]
        images = [Image.open(frame) for frame in frames]
        gif_path = os.path.join(resultFolder, "animation.gif")
        images[0].save(gif_path, save_all=True, append_images=images[1:], duration=100, loop=0)

        ui.messageBox('Finished.')
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
```
try~exceptの使い方がいい加減すぎますね。

gifの生成方法に関してはcursorエディタ上のGPT-4oさんにほとんど書いてもらいました。私はAcceptをクリックしただけです。

## ざっくり使い方
- 一番親（？）のジョイントを1つ選択する (※複数の動力には非対応)
- スクリプトを実行する
  - フレーム画像の保存先フォルダを選択する
  - しばらく待つ
- (上記フォルダパス配下にanimation.gifが生成される)

![スクリプトの実行前に一番親のジョイントを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781638/c4cd1a4d-7eb9-519a-5481-953e7f6a95ca.png)
上記の状態でスクリプトを実行します。

## コードの解説
~~詳細な解説は後日追記します…すみません。~~ 
→ 追記しました（2024/8/18）

主なポイントは以下の2点で、
 - ジョイントアニメの開始：
   - app.executeTextCommand(u"Commands.Start AnimateModelCmd") 
 - png画像での画面の保存：
   - app.activeViewport.saveAsImageFile(filename, 0, 0)

…です。

### ジョイントアニメの開始　（2024/8/18追記）
ジョイントのアニメーションの開始には、テキストコマンドというFusion360の搭載機能を使用しました。

このテキストコマンドとは、簡単に言えば、Fusion360上での各種操作をその操作に1対1対応した文字列で呼び出せる機能のことです。
テキストコマンドの詳細については [こちら][C#ATIA] の方のブログ解説が参考になります。

”押し出し”などのモデリング用の操作は勿論のこと、**左側のツリー上の項目を右クリックして現れるメニューにある各種操作も、テキストコマンドの機能を使って呼び出せる**ようです。

デザイン画面の左にあるツリーの”ジョイント”に関する右クリックメニューに、「ジョイントの関係をアニメーション表示する」という機能があります↓。

![ジョイントの関係をアニメーション表示する.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781638/fd763ed5-434a-4b0a-8c4b-9987fb04b7ba.png)

こちらをテキストコマンドを使ってPythonスクリプトから呼び出すことで、アニメーション表示させました。

```python:RecordJointMotions.py
app.executeTextCommand(u"Commands.Start AnimateModelCmd")
```

なお、テキストコマンドにおいて「ジョイントの関係をアニメーション表示する」のコマンド名は「AnimateModelCmd」です。このコマンド名については、「ジョイントの関係をアニメーション表示する」を実行中に、

```
UI.CurrentCommandInfo
```

…を、画面下にあるテキストコマンドの入力領域から入力することで調べました。下の画像のように、現在実行中の操作に対応するテキストコマンドにおけるコマンド名が確認できます。

![CurrentCommandInfoでコマンド名を調べる.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3781638/ce6c6291-862d-00ca-7898-af1a9e9a26d9.png)

:::note warn
入力前に右下のラジオボタンを「Txt」にすることも忘れずに！
:::

### png画像での画面の保存　（2024/8/18追記）
テキストコマンドを使ってFusion360 APIからアニメーション表示させることができたので、このFusion360のデザイン画面を一定間隔でキャプチャし複数枚の画像に出力できれば、それらを使ってgifアニメが作れそうです。

そこで、現在のデザイン画面をキャプチャする手段として、Fusion360 APIのViewPortクラスを使用しました。
このクラスのメンバにsaveAsImageFileというメソッドがあり、こちらを呼び出すことで現在のデザイン画面を画像ファイルとして保存できます。

```python:RecordJointMotions.py
app.activeViewport.saveAsImageFile(filename, 0, 0)
```

### gifアニメの作成　（2024/8/18追記）
複数枚のデザイン画面の画像が保存できたので、あとはそれをgifにするだけです。

gifの生成方法に関しては、冒頭にも書きましたが、cursorエディタ上のGPT-4oさんにほとんど書いてもらいました。**私はAcceptをクリックしただけです。** 以下のような簡潔なコードが生成されました。素敵。

```python:RecordJointMotions.py
frames = [os.path.join(resultFolder, "frame" + str(i).zfill(4) + ".png") for i in range(loopCnt)]
images = [Image.open(frame) for frame in frames]
gif_path = os.path.join(resultFolder, "animation.gif")
images[0].save(gif_path, save_all=True, append_images=images[1:], duration=100, loop=0)
```

また、gifアニメの作成にPillowモジュールを使用しましたので、これがインストール済かどうかをコード上でチェックするようにしました。具体的には、try~except構文を使い、from PIL import Image が成功するか失敗するかで判定しています。

```python:RecordJointMotions.py
pyDir = os.getcwd() + "\\Python\\"

try:
    # try to import PIL
    from PIL import Image
except:
    if ui:
        ui.messageBox('Failed to import PIL!')
        ui.messageBox(
            'Please install Pillow by the following command:\n'
            f'{pyDir}python.exe -m pip install Pillow'
        )
        return
```

## おわりに
TextCommand便利ですね。

それにしてもAnimationモード不便すぎやしないでしょうか…？
今回ご紹介したスクリプトで、不便さが少しでも解消されると良いなと思います。

最後まで読んで下さりありがとうございました。

:::note warn
まだ上記画像の構成でしか試していませんので、お手元のCADモデルの構成次第では上手く動かないケースがあるかもしれません。ご了承ください。
:::

## 参考リンク
スクリプトの作成にあたり、以下のサイトを参考させて頂きました。

[Fusion360 Product Documentation][FusionDoc]
  - 主にViewPortクラスの使い方に関して

[C#ATIA]
  - 主にTextCommandの使い方に関して