広告

Fusion360 APIでTkinterを使ってみた。単純UIならこれでよくない?ボタンクリックでFusionスクリプトを実行させてみた

AI

少し前から、Python 学習の一環として AI に相談しながら Fusion 360 の API を触っており、その過程を以前の記事で紹介しました。

この記事では、Fusionの標準APIの2D スケッチで頻繁に使うオブジェクトをカプセル化しライブラリとしてまとめてみました。
Fusion 360 はパラメトリックモデリングを前提としているため、まずラフに図形を描き、あとから寸法線や拘束を追加して形状を詰めていく、という作業フローが基本です。
試行錯誤しながら設計を煮詰めていく用途では非常に強力な手法ですが、CAM 作業のように、すでに図面や数値が確定している場合には、この手順がやや回りくどく感じることもあります。
またFusion 360 には原点の概念はありますが、原点を強く意識しないモデリング手法であるため、「図面ありき」の CAM 作業では、座標原点を通る中心線作図から始めるようにしています
ところが、この最初に描いた中心線も拘束や固定を忘れると、気づかないうちに移動させてしまい、後工程で大きなトラブルになることがあります。

固定された座標軸を、簡単かつ確実に作図したい
  ──まずこれが、スクリプト作成を始めたきっかけです。

また、スケッチ平面に対して高低差のある傾いた線を引きたい場合や、
平面からの異なる高さに円を描きたい場合なども、標準操作では少し手間がかかります。
前回作成したライブラリでは、直線や円といったシンプルな作図であれば、数値を引数として渡すだけで描画できるところまでは実現しています。
ただし、実用面を考えると、やはり GUI ダイアログ形式で数値入力できる仕様にしたいというのが今回の取り組みです。

Fusion API標準機能のコマンド UIでダイアログ仕様

Fusion API には、コマンドに付随する入力ダイアログ機能が標準で用意されており、テキストボックスやドロップダウンリスト、スライダーといった一般的な UI 要素を利用できます。
これらの UI 要素は Command Inputs オブジェクトを使って定義します。
コマンドが作成される際に呼び出される CommandCreated イベントハンドラ内で Command Inputs を作成し、コマンドの入力ダイアログに表示する入力フィールドを定義します。
その後、Execute イベントハンドラ内で、ユーザーがダイアログ上で入力した値を取得し、処理を実行します。
このように Fusion API のコマンドはイベント駆動で構成されており、各イベントハンドラを通して入力値を扱う必要があります。
とりあえずコード化してみましたが、初級者の私にとっては処理の流れを理解するのが難解でとりあえずネット情報やAIを参考に作成してみました
下記は、標準コマンドダイアログへの入力値を上記で紹介している「lib_2d.draw_center_line(x, y, z)スクリプトに取り込んでX,Y,Z座標軸線を作図させるスクリプトです

class CommonAddinFramework:
 ”””
 Fusion 360 アドインの共通フレームワーククラス。
 ダイアログの表示、入力値の取得および指定された描画コールバック関数の実行を管理
 ”””
 def __init__(
  self,
  command_id: str,
  button_name: str,
  description: str,
  input_defaults: dict,
  drawing_callback,
 ):

・・・・
・・・・

コード全体を表示

単純入力フォームに、tkinter を使ってみる

上記でFusion標準のコマンドUIを使って、寸法入力で座標軸線を作画できるようになりましたが、今後機能を増やしていくには、やはりイベント処理ではわかり辛く煩わしく感じますし、ネット上の情報も少ないです
そこで、Python標準のtkinterを使ってみました
tkinterはpython標準ライブラリなので、さすがにこちらは、情報豊富です

中心線作図スクリプト

かなりAIに助けてもらいましたが
上記記事内で作成した中心線スクリプトに入力数値を渡すダイアログをtkinterを利用して作成してみました。
最初にメッセージ表示の「label」を配置して、「entry」で設定値入力ボックスを配置
さらに、固定とコンストラクション設定用の「Checkbutton」を配置して作成ボタンで、作図させる仕様です

# ——————————————————————
# ダイアログクラス
# ——————————————————————
class DialogPanel(tk.Tk):
 def __init__(
  self,
  input_defaults: Dict[str, float],
  construction,
  fixed,
 ):

・・・・
・・・・

コード全体を表示

この「DialogPanel」クラスをアレンジして、直線や円などを作図する入力ダイアログも追加作成していきます。
このスクリプトをFuisonの「スクリプトとアドイン」に登録すれば、作図スクリプトをダイレクト数値入力仕様で実行できるようになります。
私的には、FusionAPIのCommand Inputsよりもわかりやすいです。

表示されたボタンをクリックして、関数を実行するスクリプト

いくつかコマンドダイアログを作成していくと、いちいち「スクリプトとアドイン」へ登録するのも面倒になってきます。
FusionAPIでもFusionタスクバーにコマンド実行プルダウンを追加できるようですが、これもまた敷居が高いです。
今回は、tkinterでメニュー用パネルを作成し、そのメニューボタンで作図スクリプトを起動できれば、メニューだけの登録で済みます。

ボタンクリックで起動させる、tk.Button の command オプション

ボタンクリックで関数を実行させるには「command」オプションに関数名を渡すと実行できます

# 起動させたい関数
def cmd1():
    print("Function is cmd1().")
・・・
・・・
# ボタンクリックで、「def cmd1()」関数を起動する
tk.Button(
        root,
        text="Execute_command01",
        command=cmd1
)

ここで初心者の筆者が疑問に感じたのが、「command=cmd1」の書き方です
関数名は「cmd1()」なのに、「commandオプション」には”( )”が付かないようです
この関数名の”( )”の有無について、AIに聞いてみました

関数名の () の有無による違い

1. かっこがない場合 (cmd1)

かっこがない場合、それは関数そのもの(オブジェクト) を指します。これは「関数というデータ」として扱われます。

意味関数そのものを指す(データ)
data = { "menu1": cmd1 }
目的関数を変数に代入したり、tk.Buttoncommand のように他の関数に引数として渡すとき。
動作処理はまだ実行されない。ただ関数の場所を渡すだけ。

2. かっこがある場合 (cmd1())

かっこがある場合、それは関数の実行(呼び出し) を意味します。

意味関数の処理を今すぐ実行する
cmd1()
目的関数に書かれている処理をすぐに実行し、その戻り値(結果) を受け取るとき。
動作すぐに処理が実行される。もし戻り値があれば、その結果が残る。

なるほど~
“( )”がない場合には、C言語でのポインタ渡しのようなイメージですかね
“( )”を付けると、「すぐに実行される」との事なので、
クリック後実行させたい、「tk.Button」の「command」オプションには、使えないですね
したがってTkinterのcommand は、引数なしで呼び出せるものを要求すると言うことになります
では、引数や戻り値を利用したい関数を実行するにはどうするか?

lambda(ラムダ式)を使う

lambda は、短い無名関数(名前のない一時的な関数)を簡単に作るためのPythonの機能ですが、デフォルト引数を設定できるけど、実行時には引数なしで呼び出されるようです
したがって、「tk.Button」の「command」に利用できます
具体的には、「lambda 引数1,引数2…:引数を使った処理」のように記述します

def exec(function, msg):
    function(msg)
・・・・・・
・・・・・・
command = lambda msg="button click!", func=cmd1: exec(func, msg)

このように、関数:execに、func, msg の引数を渡せば
「func名」の関数に「引数msg」を渡して実行できます
簡単な、サンプルコードを書いてみました

"""Execute a function on button click."""

import traceback
import adsk.core
import adsk.fusion

# import adsk.cam

import tkinter as tk


def disp_message(msg: str):
    app = adsk.core.Application.get()
    ui = app.userInterface
    ui.messageBox(f"{msg}")


def print_cmd1():
    disp_message("lambda is not used.")


def print_cmd2(msg: str):
    disp_message(msg)


def exec(function, msg):
    function(msg)


def run(context):
# def run():
    root = tk.Tk()
    label1 = tk.Label(
        root,
        text="Click the button.",
        bg="light cyan",
    )
    label1.pack()
    # ---------------------------------
    tk.Button(
        root,
        text="Execute1",
        command=print_cmd1,
    ).pack(side=tk.LEFT, padx=10, pady=10)
    # ---------------------------------
    tk.Button(
        root,
        text="Execute2",
        command=lambda m="center button click!", f=print_cmd2: exec(f, m),
    ).pack(side=tk.LEFT, padx=10, pady=10)
    # ---------------------------------
    tk.Button(
        root,
        text="Execute3",
        command=lambda m="right button click!", f=print_cmd2: exec(f, m),
    ).pack(side=tk.LEFT, padx=10, pady=10)
    root.mainloop()

こんな感じで、「ボタン」を並べれば、ボタンクリック仕様のコマンド実行メニューが作成できそうです

Tkinterは、Autodesk 非公式

「tk.Button」と「lambda」で、メニューダイアログが作れそうです
上記で紹介した座標軸センターラインを作図するスクリプトを実行するダイアログを作成してみます
ところが、Tkinterの使用は、Autodeskは非公式のようなので注意が必要です

Fusion API で、tkinter を使用する場合の注意点

・Tkinterの使用は、Autodesk 非公式
・Tkinter が root.mainloop() で動作中は Fusion 本体の操作は一切できなくなる
・Mac + Tkinter は挙動が不安定なことがある
・Tkinterウィンドウを閉じた時に不安定になる場合がある
・最悪、タスクマネージャーでFusion 強制終了

やはり、Autodesk側としては、あまり推奨はしていないようです
常駐ツールやモーダルな本格的ダイアログはやめたほうがよさそうです
今回作成中も、閉じる時に「Script Error」が出たり、強制終了の必要に迫られたりしましたが、数値入力や条件入力の簡易パネルとして、開いたら閉じて、Fusionに制御を返すような単純な処理であれば利用できると思っています。

run(context) の context引数 は必須

python学習もかねて、いろいろ簡単なコードを書きました。
コードが悪いと、Fusionを強制終了させざるを得ない場合もありましたが、普通に問題なく動作しているのに、パネルの「×ボタン」で終了時に、Fusionのコマンドエリアに「SCRIPT ERROR」が出た事もありました。
これの原因究明には結構時間がかかりました。
上記のサンプルコードでも、run(context)の「context」は使っていないからと、
省略すると(「def run():」)「SCRIPT ERROR」になり、なにも動作しない状態になります
やはり、FusionAPIがどこかで使っていて、必須なようです
「×」での「SCRIPT ERROR」は終了時の警告のみなので、無視しようとも思いましたが、気味が悪いので、とりあえずAIにもいろいろも相談しましたが、的確な回答はもらえませんでした。
例えば下記コードで終了時に「SCRIPT ERROR」になります。
引数に「tk.Tk()型」を渡しました。結局、この引数が原因でした

def run1(root):
     label1 = tk.Label(root , text=”Click the button.”)
  ・・・・
  ・・・・

master = tk.Tk()
run1(master)

Fusionの「スクリプトとアドイン」で、「スクリプトまたはアドインを作成」を選択すると、自動的にサンプルスクリプトが作成されるので、そちらを参考にしてみました。
Fusionで自動作成された、run関数の中身は下記のようになっています

def run(_context: str):
    """This function is called by Fusion when the script is run."""

    try:
        # Your code goes here.
        ui.messageBox(f'"{app.activeDocument.name}" is the active Document.')
    except:  #pylint:disable=bare-except
        # Write the error message to the TEXT COMMANDS window.
        app.log(f'Failed:\n{traceback.format_exc()}')

引数は「_context: str」となっています。
以前のサンプルコードには、「str」のような型ヒントはなかったように記憶していますが
「str」なので引数の型は「文字列型」を期待しているようです。
このサンプルでも「_context」はどこにも使われていないように見えますが、やはりFusionAPI側では必須なのでしょう
ちなみにデフォルトでは「_context: str」と、型ヒントでは「文字列型」となっていますが、下記コードを追加して調べてみました

    ui.messageBox(f"type={type(_context)}")
    ui.messageBox(f"{_context}")

そのタイプと内容を表示させてみると

type=<class ‘dict’>
{‘IsApplicationStartup’: False}

このように、「辞書型」になっていました。
詳細はわかりませんが、メイン関数の第一引数はFusionが使うので、別の引数を使いたい場合には、追加の形をとったほうがよさそうです
この部分を変更すると正常に終了するようになりました。
pythonのこういった型のあいまいな部分にも、まだまだ慣れないです

数値入力ダイアログで、座標軸を作図するスクリプトをボタンで起動

さて、上記で紹介した「中心線作図スクリプト」を、ボタンクリックで動作させるダイアログを作成してみようと思います。
ただし、このスクリプトは、以前紹介した記事内のライブラリ「lib_util.py」「lib_2d.py」を利用するので、もし試してみたい場合にはそちらも必要になります
下記に全部含まれたコードをダウンロードできるようにしておきますので、興味ある方がいらっしゃれば試してみてください。

"""Execute a function on button click."""

import adsk.core
import adsk.fusion
import os, sys, importlib, traceback
import tkinter as tk

import tkinter.font as tkFont
from typing import Optional, List, Dict, Any

current_dir = os.path.dirname(os.path.abspath(__file__))
script_dir = os.path.dirname(current_dir)  # C:\FusionAPI\Scripts
sys.path.append(script_dir)
# library import
"""
C:\FusionAPI\Scripts
│
├─library
│  ├─lib_util.py
│  ├─lib_2d.py
├─test
│  ├─menu.py (This script)
"""
from library import lib_util, lib_2d


##---------------------------
def disp_message(msg: str):
    lib_util.disp_message(msg)


# ------------------------------------------------------------------
# ダイアログクラス
# ------------------------------------------------------------------
class DialogPanel(tk.Tk):
    """センターライン作画ダイアログ"""

    def __init__(
        self,
        input_defaults: Dict[str, float],
        construction,
        fixed,
    ):
        super().__init__()
        # self.my_font = None
        self.input_defaults = input_defaults
        self.x_width: float = 0.0
        self.y_width: float = 0.0
        self.z_width: float = 0.0
        self.construction: bool = construction
        self.fixed: bool = fixed
        self.widths: dict[str, tk.DoubleVar] = {}
        self._create_widgets()

    def _simple_frame(self, **kwargs) -> tk.Frame:
        # Fontインスタンスを作成
        self.my_font = tkFont.Font(family="Arial", size=10, weight="normal")
        """単純フレームを作成し、ウィンドウ全体に展開"""
        master = self
        frame = tk.Frame(master, **kwargs)
        master.title("Center Line")
        frame.pack(fill="both", expand=True, padx=5, pady=5)
        return frame

    def _create_widgets(self):
        """全てのUI要素を作成"""
        # 全体を包む単一のフレーム
        self.main_frame = self._simple_frame(relief="ridge", bd=5)

        # ------------------
        # 0. トップメッセージ
        # ------------------
        row_idx = 0
        top_frame = tk.Frame(self.main_frame)
        top_frame.grid(row=row_idx, column=0, columnspan=3, pady=10)
        tk.Label(
            top_frame,
            text="各軸の長さを指定してください。\n作成したくない軸は「0.0」にしてください",
            bg="light cyan",
            font=self.my_font,
        ).pack()
        # ------------------
        # 1~3 各軸幅入力ボックス
        # ------------------

        row_idx += 1
        # 事前設定のラベルとデフォルト値
        for label, default in self.input_defaults.items():
            tk.Label(
                self.main_frame,
                text=label,
                font=self.my_font,
            ).grid(
                row=row_idx,
                column=0,
                sticky="e",
            )
            var = tk.DoubleVar(value=default)
            ent = tk.Entry(
                self.main_frame,
                font=self.my_font,
                textvariable=var,
                width=10,
                bg="lightyellow",
                # bg="ivory",
            )
            ent.grid(row=row_idx, column=1)
            self.widths[label] = var
            row_idx += 1
        # ------------------
        # 4. 要素固定チェック
        # ------------------
        row_idx += 1
        # self.fixed_check = tk.BooleanVar(value=True)
        self.fixed_check = tk.BooleanVar(value=self.fixed)
        check_button = tk.Checkbutton(
            self.main_frame,
            text="要素固定",
            variable=self.fixed_check,
            font=self.my_font,
        )
        check_button.grid(row=row_idx, column=0, padx=5, pady=5, sticky="w")
        # self.construction_check = tk.BooleanVar(value=True)
        self.construction_check = tk.BooleanVar(value=self.construction)
        check_button = tk.Checkbutton(
            self.main_frame,
            text="コンストラクション",
            variable=self.construction_check,
            font=self.my_font,
        )
        check_button.grid(row=row_idx, column=1, padx=5, pady=5, sticky="w")

        # ------------------
        # 5. 最終確定ボタン
        # ------------------
        row_idx += 1
        # ボタンを格納するボトムフレーム
        bottom_frame = tk.Frame(self.main_frame)
        bottom_frame.grid(row=row_idx, column=0, columnspan=3, pady=10)

        tk.Button(
            bottom_frame, text="作成実行", font=self.my_font, command=self.get_data
        ).pack(side=tk.LEFT, padx=10)
        tk.Button(
            bottom_frame, text="閉じる", font=self.my_font, command=self.destroy
        ).pack(side=tk.LEFT, padx=10)

    def get_data(self):
        """入力データを取り出す"""
        v: List = []
        for value in self.widths.values():
            v.append(value.get())
        self.x_width = float(v[0])
        self.y_width = float(v[1])
        self.z_width = float(v[2])
        self.fixed = self.fixed_check.get()
        self.construction = self.construction_check.get()
        self.destroy()


##--------------------------------------------------------------
##--------------------------------------------------------------
def diarog_test(master):
    """センターライン作画"""
    if master:
        master.quit()  # mainloop を終了させる
        master.destroy()
    input_defaults = {
        "X軸長さ": 50.0,
        "Y軸長さ": 50.0,
        "Z軸長さ": 10.0,
    }
    # 線種指定
    construction = True
    fixed = True

    app = DialogPanel(input_defaults, construction, fixed)
    app.mainloop()
    wx = app.x_width
    wy = app.y_width
    wz = app.z_width
    construction = app.construction
    fixed = app.fixed
    try:
        lib_2d.draw_center_line(wx, wy, wz, construction, fixed)
    except:
        disp_message("Failed:\n{}".format(traceback.format_exc()))


# ---- Action command after button click ----------


def print_cmd1():
    disp_message("lambda is not used.")


def print_cmd2(msg: str):
    disp_message(msg)


def exec(function, msg):
    function(msg)


def on_closing(root):
    """Processing when the window's 'x' button is pressed"""
    disp_message("I'll close now!")
    root.quit()  # Terminate the mainloop
    root.destroy()  # Releases window resources.


def button(root: tk.Tk):
    # Window close action
    root.protocol("WM_DELETE_WINDOW", lambda: on_closing(root))
    label1 = tk.Label(root, text="Click the button you like.", bg="light cyan")
    label1.pack()
    tk.Button(root, text="Message1", command=print_cmd1).pack(
        side=tk.LEFT, padx=10, pady=10
    )
    tk.Button(
        root,
        text="Message2",
        command=lambda m="Lambda specification!", f=print_cmd2: exec(f, m),
    ).pack(side=tk.LEFT, padx=10, pady=10)
    tk.Button(
        root,
        text="Draw center line",
        command=lambda tk=root, f=diarog_test: exec(f, tk),
    ).pack(side=tk.LEFT, padx=10, pady=10)
    root.mainloop()


def run(context: dict):  # {'IsApplicationStartup': False}
    root = tk.Tk()
    root.attributes("-topmost", True)  # Bring Window To Top
    button(root)

サンプルスクリプトのダウンロードと使い方

ダウンロードと使用方法は下記を参考にしてください
この例では、展開されたトップフォルダ「FusinAPI_kazuban」を「Cドライブ」の直下に、コピーする方法で説明していますが、任意の場所でかまいません
ただし、Fusionが立ち上がった状態では、キャッシュの影響で「library」内のスクリプトがうまく読み込めない事があります
コピー後に、Fusionを起動する方がトラブル少ないです。
そのトップフォルダ下の「Scripts」、その下の「library」「menyu01」内のスクリプトを使用します
全ての使用ライブラリも含めたZIPファイルは下記からのダウンロードできます

C:\FusinAPI_kazuban
└─Scripts
├─library
└─lib_2d.py
└─lib_util.py
└─menyu01
└─menu.py

・ダウンロード後展開した「FusinAPI_kazuban」を適当なフォルダへコピー
・Fusionの「スクリプトとアドイン」から「menyu01」を選択する
・正常に登録されると、スクリプトが実行できます

第一弾、終了

とりあえず、Autodesk Fusion からtkinterオブジェクトのパネルを表示し、ボタンに割り当てられたスクリプト動作をさせる事ができました。
数値入力用ボタンでは、tkinterへの入力値をFusionAPIに渡し、作図させる事ができるようになりました
次回は、もう少し作図スクリプトとメニューを充実させていきたいと思います

コメント

タイトルとURLをコピーしました