C#ATIA

↑タイトル詐欺 主にFusion360API 偶にCATIA V5 VBA(絶賛ネタ切れ中)

ダイアログなスクリプト入門3

こちらの続きです。
ダイアログなスクリプト入門2 - C#ATIA
予告を変更しました。(大体そんなものですよね・・・)

前回は、SelectionCommandInputを実行し選択出来るようにしました。
フィルタやリミットにより動作に制限も付けましたが、未だに実用的な
物とは程遠いです。
今回は選択時に発火するイベントと他のCommand Inputsとの連携を例にして
記載したいと思います。

今回のゴール

レイアウトと動作の関係からテキストとセレクションを入れ替えて、
この様な状態をゴールとします。
f:id:kandennti:20220118093718p:plain
前回のフィルタを流用して点を選択し、ダイアログ上に座標値を表示させます。

ベースとなるコード

基本的には前回の物を流用していますが、不要そうなコメントは削除しています。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

_app: adsk.core.Application = None
_ui: adsk.core.UserInterface = None

_handlers = []


def run(context):
    try:
        global _app, _ui
        _app = adsk.core.Application.get()
        _ui = _app.userInterface

        cmdDef: adsk.core.CommandDefinition = _ui.commandDefinitions.itemById(
            'test_cmd_id'
        )

        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                'test_cmd_id',
                'ダイアログです',
                'ツールチップです'
            )

        global _handlers
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        _handlers.append(onCommandCreated)

        cmdDef.execute()

        adsk.autoTerminate(False)

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


class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers

            cmd: adsk.core.Command = adsk.core.Command.cast(args.command)

            # event
            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

            # inputs
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            selIpt: adsk.core.SelectionCommandInput = inputs.addSelectionInput(
                'selIpt',
                '点を選択して下さい',
                '点を選択して下さい'
            )
            selIpt.addSelectionFilter('SketchPoints')
            selIpt.addSelectionFilter('ConstructionPoints')
            selIpt.setSelectionLimits(0)

            txtIpt: adsk.core.TextBoxCommandInput = inputs.addTextBoxCommandInput(
                'txtIpt',
                '座標値',
                '-',
                1,
                True
            )

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

class MyExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)


class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        adsk.terminate()

InputChangedイベントにHandlerの追加

今回の目的は、点を選択する度に座標値を取得し表示させます。
その為、選択が変更された状態を取得する必要がありますが、それを実現させるのが
InputChangedイベントです。
Fusion 360 Help
ドキュメントが更新されると言う、想定していなかった事が起きたのですが、
見て頂きたいのは "Python" タブ部分です。
f:id:kandennti:20220118093811p:plain
又、赤矢印部分には閉じるカッコ ")" が必要です。(そのうち修正されるはず)

InputChangedイベントは、Command Inputsの状態が変化した際に発火するイベントです。
これはダイアログ上の全てのCommand Inputsが対象です。
CommandCreatedHandlerに、ドキュメントに従って次のように追加します。

・・・
class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
・・・
            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

            # ダイアログの全てのCommand Inputsの状態が変化した場合に
            # 発火するイベントにハンドラを登録
            onInputChanged = MyInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            _handlers.append(onInputChanged)
・・・

InputChangedHandlerの作成

順番が逆になりますがハンドラを作成します。
異本的にはドキュメントの記載をベースにします。

・・・
# ダイアログの全てのCommand Inputsの状態が変化した場合に発火する
# イベントに対してのハンドラ
class MyInputChangedHandler(adsk.core.InputChangedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.InputChangedEventArgs):
・・・

他のイベントでも共通ですが、イベントが発火した際にはnotifyメソッドが
呼び出されます。
呼び出された際、必ず "args" パラメータも付いています。
これは発火するイベントによって異なりますが、今回はInputChangedEventArgsを
受け取ります。
Fusion 360 Help
正直な所、argsについてはドキュメントを見るより、ブレークポイント
止めてしまい、VSCodeの変数を辿って行った方が理解も早いです。

InputChangedイベントは、全てのCommand Inputsの変更で発火します。
今回のダイアログ上にはSelectionCommandInput以外にもテキストが
配置されていますが、テキストの変更時には特に行うべき処理は無い為、
早々とreturnしてしまいます。

・・・
    def notify(self, args: adsk.core.InputChangedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # 今回はadsk.core.SelectionCommandInputの変更以外は
        # 受け付けない事にしています。
        if args.input.classType() != adsk.core.SelectionCommandInput.classType():
            return
・・・

続いて何を選択しているかを取得する必要があります。
adsk.core.InputChangedEventArgsに関しては、args.inputが変更されイベントを
発火させたCommand Inputsとなります。

又、座標値を更新する為のTextBoxCommandInputも取得する必要があります。
こちらは発火したイベントとは直接関係ありません。
その為、ダイアログに配置されているCommand Inputsの中からIDを利用して
取得を行います。

・・・
        # ダイアログのTextBoxCommandInputの取得
        # IDから取得しています
        txtIpt: adsk.core.TextBoxCommandInput = args.inputs.itemById('txtIpt')
・・・

その為、CommandCreatedイベント時にCommand Inputsを追加していますが、
それぞれのIDはユニークな値にしておく必要が有ります。

選択フィルタでスケッチの点とコンストラクションの点を許可している為、
座標値の取得に関してはそれぞれ異なる方法で取得する必要がありますが、
それらは関数化して処理する事にします。
この処理については今回のテーマではない為、割愛します。

最後に取得した座標値は単位付きの文字列のリストとして取得している為、
TextBoxCommandInputのtextプロパティを書き換えて表示させています。

・・・
        # テキストの表示を更新する
        txtIpt.numRows = len(pnts)
        txtIpt.text = '\n'.join(posList)
・・・

最終的にはInputChangedHandlerは、この様になりました。

・・・
# ダイアログの全てのCommand Inputsの状態が変化した場合に発火する
# イベントに対してのハンドラ
class MyInputChangedHandler(adsk.core.InputChangedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.InputChangedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # 今回はadsk.core.SelectionCommandInputの変更以外は
        # 受け付けない事にしています。
        if args.input.classType() != adsk.core.SelectionCommandInput.classType():
            return

        # 今回発火したCommand Inputs
        selIpt: adsk.core.SelectionCommandInput = args.input

        # ダイアログのTextBoxCommandInputの取得
        # IDから取得しています
        txtIpt: adsk.core.TextBoxCommandInput = args.inputs.itemById('txtIpt')

        # 選択されている数をチェックします。
        # 全く選択されていない場合はテキストの表示を"-"にします。
        if selIpt.selectionCount < 1:
            txtIpt.numRows = 1
            txtIpt.text = '-'
            return

        # 扱いが悪いので一度選択された要素をリストに代入します
        pnts = [selIpt.selection(idx).entity for idx in range(
            selIpt.selectionCount)]

        # それぞれの座標値を単位付きの文字列として取得
        posList = [self.getPosition(p) for p in pnts]

        # テキストの表示を更新する
        txtIpt.numRows = len(pnts)
        txtIpt.text = '\n'.join(posList)

    def getPosition(self, entity) -> str:
        # オブジェクトの型別にジオメトリの取得
        # 表示させる単位をユーザーが使用している単位に合わせたい為
        # UnitsManagerも取得
        # ※Fusion360APIで使用されている内部の単位はCmです。
        geo: adsk.core.Point3D
        unitMgr: adsk.core.UnitsManager
        if entity.classType() == adsk.fusion.SketchPoint.classType():
            geo = entity.worldGeometry
            unitMgr = entity.parentSketch.parentComponent.parentDesign.unitsManager
        elif entity.classType() == adsk.fusion.ConstructionPoint.classType():
            geo = entity.geometry
            unitMgr = entity.parent.parentDesign.unitsManager
        else:
            return '-'

        # 座標値を単位付きで返す
        return 'x:{} y:{} z:{}'.format(
            unitMgr.formatInternalValue(geo.x),
            unitMgr.formatInternalValue(geo.y),
            unitMgr.formatInternalValue(geo.z),
        )
・・・

不要なOKを非表示にする

一度、ここまで作成したものを実行してみる事にします。
適当にスケッチの点やコンストラクション点を複数作成し、スクリプトを実行して選択してみます。
f:id:kandennti:20220118094043p:plain
取りあえず選択した点の座標値が表示されます。

これで問題は無いのですが、今回のダイアログは座標値の表示するだけの為、OKボタンが
不要です。
OKボタンを非表示する事はCommand.isOKButtonVisibleを利用すると可能なので、
非表示にした方がより親切でしょう。
Fusion 360 Help
又、OKボタンを使わない以上、executeイベント自体も不要ですので、ハンドラも削除して
しまいましょう。

・・・
class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
・・・
    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers

            cmd: adsk.core.Command = adsk.core.Command.cast(args.command)

            # 今回はOKボタンが不要な為、非表示にする
            cmd.isOKButtonVisible = False

            # event
            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            # onExecute = MyExecuteHandler()
            # cmd.execute.add(onExecute)
            # _handlers.append(onExecute)
・・・

これで最初に示したゴールの状態のダイアログが完成しました。

まとめ

全てのコードは以下のようになります。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

_app: adsk.core.Application = None
_ui: adsk.core.UserInterface = None

_handlers = []


def run(context):
    try:
        global _app, _ui
        _app = adsk.core.Application.get()
        _ui = _app.userInterface

        cmdDef: adsk.core.CommandDefinition = _ui.commandDefinitions.itemById(
            'test_cmd_id'
        )

        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                'test_cmd_id',
                'ダイアログです',
                'ツールチップです'
            )

        global _handlers
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        _handlers.append(onCommandCreated)

        cmdDef.execute()

        adsk.autoTerminate(False)

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


class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers

            cmd: adsk.core.Command = adsk.core.Command.cast(args.command)

            # 今回はOKボタンが不要な為、非表示にする
            cmd.isOKButtonVisible = False

            # event
            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            # ダイアログの全てのCommand Inputsの状態が変化した場合に
            # 発火するイベントにハンドラを登録
            onInputChanged = MyInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            _handlers.append(onInputChanged)

            # inputs
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            selIpt: adsk.core.SelectionCommandInput = inputs.addSelectionInput(
                'selIpt',
                '点を選択して下さい',
                '点を選択して下さい'
            )
            selIpt.addSelectionFilter('SketchPoints')
            selIpt.addSelectionFilter('ConstructionPoints')
            selIpt.setSelectionLimits(0)

            txtIpt: adsk.core.TextBoxCommandInput = inputs.addTextBoxCommandInput(
                'txtIpt',
                '座標値',
                '-',
                1,
                True
            )

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

# ダイアログの全てのCommand Inputsの状態が変化した場合に発火する
# イベントに対してのハンドラ
class MyInputChangedHandler(adsk.core.InputChangedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.InputChangedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # 今回はadsk.core.SelectionCommandInputの変更以外は
        # 受け付けない事にしています。
        if args.input.classType() != adsk.core.SelectionCommandInput.classType():
            return

        # 今回発火したCommand Inputs
        selIpt: adsk.core.SelectionCommandInput = args.input

        # ダイアログのTextBoxCommandInputの取得
        # IDから取得しています
        txtIpt: adsk.core.TextBoxCommandInput = args.inputs.itemById('txtIpt')

        # 選択されている数をチェックします。
        # 全く選択されていない場合はテキストの表示を"-"にします。
        if selIpt.selectionCount < 1:
            txtIpt.numRows = 1
            txtIpt.text = '-'
            return

        # 扱いが悪いので一度選択された要素をリストに代入します
        pnts = [selIpt.selection(idx).entity for idx in range(
            selIpt.selectionCount)]

        # それぞれの座標値を単位付きの文字列として取得
        posList = [self.getPosition(p) for p in pnts]

        # テキストの表示を更新する
        txtIpt.numRows = len(pnts)
        txtIpt.text = '\n'.join(posList)

    def getPosition(self, entity) -> str:
        # オブジェクトの型別にジオメトリの取得
        # 表示させる単位をユーザーが使用している単位に合わせたい為
        # UnitsManagerも取得
        # ※Fusion360APIで使用されている内部の単位はCmです。
        geo: adsk.core.Point3D
        unitMgr: adsk.core.UnitsManager
        if entity.classType() == adsk.fusion.SketchPoint.classType():
            geo = entity.worldGeometry
            unitMgr = entity.parentSketch.parentComponent.parentDesign.unitsManager
        elif entity.classType() == adsk.fusion.ConstructionPoint.classType():
            geo = entity.geometry
            unitMgr = entity.parent.parentDesign.unitsManager
        else:
            return '-'

        # 座標値を単位付きで返す
        return 'x:{} y:{} z:{}'.format(
            unitMgr.formatInternalValue(geo.x),
            unitMgr.formatInternalValue(geo.y),
            unitMgr.formatInternalValue(geo.z),
        )


class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        adsk.terminate()

今回は、要素の選択を検知し得られた結果をダイアログに反映しました。
次回はもう少し効果を実感出来るものにしたいですね。