C#ATIA

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

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

こちらの続きです。
ダイアログなスクリプト入門3 - C#ATIA

前回は、InputChangedイベントを取り扱いました。そろそろOKボタンを押して何らかの
処理を行いたいところです。
今回は複数のスケッチの点を選択し、選択された点を中心とする球体のボディを作成する
機能を追加する事にします。

今回のゴール

SelectionCommandInputが1個だけと言う、非常にシンプルなものです。
f:id:kandennti:20220121145511p: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.setSelectionLimits(1)

        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()

ExecuteHandlerの作成

IDでSelectionCommandInputを取得し、SelectionCommandInputから選択された
要素を取得する事は、前回も行いました。
MyExecuteHandlerをこの様に書き換えてしまいます。

・・・
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)

        # ダイアログのSelectionCommandInputの取得
        selIpt: adsk.core.SelectionCommandInput = args.command.commandInputs.itemById(
            'selIpt')

        # 選択されている点のジオメトリのリストを作成
        pnts = [selIpt.selection(idx).entity.worldGeometry for idx in range(
            selIpt.selectionCount)]

        # 球体ボディの作成
        createSphereBodies(pnts)
・・・

createSphereBodies関数は球体の中心部となるPoint3Dのリストをパラメータ
として受け取り、球体のボディを作成する関数です。
関数内の処理については今回のテーマでは無いため、説明は割愛します。
"そんなもの" 程度に思っておいてください。

・・・
def createSphereBodies(pnts: list):
    # 球体の半径 1Cm
    radius = 1

    # TemporaryBRepManagerの取得
    tmpMgr: adsk.fusion.TemporaryBRepManager = adsk.fusion.TemporaryBRepManager.get()

    # 球体の一時的なボディの作成 これは画面には表示されません
    sphereBodies: list = []
    pnt: adsk.core.Point3D
    for pnt in pnts:
        sphere: adsk.fusion.BRepBody = tmpMgr.createSphere(pnt, radius)
        sphereBodies.append(sphere)

    # ルートコンポーネントの取得
    app: adsk.core.Application = adsk.core.Application.get()
    des: adsk.fusion.Design = app.activeProduct
    root: adsk.fusion.Component = des.rootComponent

    # デザインが履歴付きかどうかを判断
    isParametric: bool = True
    if des.designType == adsk.fusion.DesignTypes.DirectDesignType:
        isParametric = False

    # 新しいコンポーネント(APIではOccurrence)の作成
    occ: adsk.fusion.Occurrence = root.occurrences.addNewComponent(
        adsk.core.Matrix3D.create()
    )

    # コンポーネントの取得
    comp: adsk.fusion.Component = occ.component

    # 履歴がある場合、ベースフューチャを作成する
    baseFeat: adsk.fusion.BaseFeature = None
    if isParametric:
        baseFeat = comp.features.baseFeatures.add()

    # ボディコレクションの取得
    bodies: adsk.fusion.BRepBodies = comp.bRepBodies

    # 球体をボディコレクションに追加する これで表示されます
    # 履歴の有無によって処理が異なる
    if isParametric:
        # パラメトリック
        baseFeat.startEdit()
        for body in sphereBodies:
            bodies.add(body, baseFeat)
        baseFeat.finishEdit()
    else:
        # ダイレクト
        for body in sphereBodies:
            bodies.add(body)
・・・

前回、座標値を取得する関数はメソッドとして作成しましたが、今回は単体の関数と
しています。ちょっと訳が有りまして・・・。

Executeイベントを実装後、実行する

スケッチを作成し、適当に点を複数作成して下さい。
スクリプトを実行し、点を選択してOKボタンを押すと球体のボディが作成されるはずです。
f:id:kandennti:20220121145637p:plain

これはこれで問題は無いのですが、通常のコマンドに比べて何か物足りないように
感じないでしょうか?
例えば、押し出しコマンドの場合プロファイルを選択し高さを入力するとOKボタンを
押さなくても押し出したボディが表示されます。プレビューですね。
f:id:kandennti:20220121145706p:plain
決定しなくても形状を見ながら調整できるので、親切な設計ですね。

executePreviewイベントとハンドラーの実装

プレビューを表示させるためには、executePreviewイベントを利用します。
CommandCreatedハンドラー内にexecutePreviewハンドラーを登録します。

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

            # executePreviewハンドラーの登録
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

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

後はハンドラーを作成しますが、特別な事はありません。
executeハンドラーと全く同じで良いのです。

・・・
# executePreviewハンドラー 実は中身はMyExecuteHandlerと全く同じ
class MyExecutePreviewHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # ダイアログのSelectionCommandInputの取得
        selIpt: adsk.core.SelectionCommandInput = args.command.commandInputs.itemById(
            'selIpt')

        # 選択されている点のジオメトリのリストを作成
        pnts = [selIpt.selection(idx).entity.worldGeometry for idx in range(
            selIpt.selectionCount)]

        # 球体ボディの作成
        createSphereBodies(pnts)
・・・

一度実行してみましょう。
f:id:kandennti:20220121145740p:plain
点を選択する度に球体が表示されます。簡単にプレビューを実装する事が出来ます。

プレビューのまま完了する

プレビューを実装させたものの、同じ内容のハンドラーを記載するのは無駄ですね。

こんな風にexecuteとexecutePreviewイベントハンドラーを共通にしてしまうのも
良いのですが、もっと良い方法があります。

・・・
            # executePreviewハンドラーの登録
            # onExecutePreview = MyExecutePreviewHandler()
            onExecutePreview = MyExecuteHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)
・・・

その前に実験してみましょう。今まで使用していた球体を作成している
createSphereBodies関数の最後に5秒間停止させるようにします。

・・・
def createSphereBodies(pnts: list):
    # 球体の半径 1Cm
    radius = 1
・・・
    if isParametric:
        # パラメトリック
        baseFeat.startEdit()
        for body in sphereBodies:
            bodies.add(body, baseFeat)
        baseFeat.finishEdit()
    else:
        # ダイレクト
        for body in sphereBodies:
            bodies.add(body)
    
    # 無駄ですが5秒停止させます
    import time
    time.sleep(5)

全く無駄な5秒なのですが、非常に複雑な処理をさせている代わりだと思ってください。

この状態で実行し点を選択すると、直ぐには表示されずに5秒待たされてプレビューが
表示されます。(そのようにしました)
ここからOKボタンを押すと確かに球体が作成されますが、再度5秒待たされてから
操作が出来るようになります。

テキストコマンドウィンドウに発生したイベントのログを表示させているようにしているので
確認してみると、スクリプトの開始からOKボタンを押して終了するまでの状態は
この様に出力されています。

 CommandCreated
 OnExecutePreview
 OnExecute
 OnDestroy

今回のサンプルでは、OnExecutePreviewとOnExecuteでは全く同じ処理を行っています。

この無駄な処理を防ぐためのプロパティが存在します。それが
CommandEventHandler.isValidResultプロパティです。
Fusion 360 Help
これをTrueにする事で、OKボタンを押すことでプレビューの状態のまま終了させることが
出来ます。

ExecutePreviewハンドラーに追加してみます。

・・・
class MyExecutePreviewHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
・・・
        # 球体ボディの作成
        createSphereBodies(pnts)

        # プレビューのまま終了させる
        args.isValidResult = True
・・・

この状態で再度実行し、イベントのログを確認すると

 CommandCreated
 OnExecutePreview
 OnDestroy

OnExecuteが消えている事が確認出来ますし、OKボタンを押した後直ぐに操作が
出来るようになっている事も体感できると思います。

と言う事は、Executeイベントハンドラー自体が不要と言う事になります。

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

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

            # executePreviewハンドラーの登録
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)
・・・

但し、今回はプレビュー時に実際に球体を作成していた為、有効な方法です。
今回のテーマではないのですが、プレビューをより高速な表示を必要とする場合に
カスタムグラフィックスを使用する方法もありますが、その場合は
executeとexecutePreviewイベントハンドラーで異なる処理が必要となる為、
isValidResultはFalse(又は記載しない)にする必要があります。

まとめ

今回は何度も書き換えてしまったので、かなり分かりにくかったと思いますが
最終的なコードは以下のようになります。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core
import time

_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)

            # executePreviewハンドラーの登録
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

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

            selIpt: adsk.core.SelectionCommandInput = inputs.addSelectionInput(
                'selIpt',
                '点を選択',
                'スケッチの点を選択して下さい'
            )
            selIpt.addSelectionFilter('SketchPoints')
            selIpt.setSelectionLimits(1)

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

# executePreviewハンドラー 実は中身はMyExecuteHandlerと全く同じ
class MyExecutePreviewHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # ダイアログのSelectionCommandInputの取得
        selIpt: adsk.core.SelectionCommandInput = args.command.commandInputs.itemById(
            'selIpt')

        # 選択されている点のジオメトリのリストを作成
        pnts = [selIpt.selection(idx).entity.worldGeometry for idx in range(
            selIpt.selectionCount)]

        # 球体ボディの作成
        createSphereBodies(pnts)

        # プレビューのまま終了させる
        args.isValidResult = True

# 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)

#         # ダイアログのSelectionCommandInputの取得
#         selIpt: adsk.core.SelectionCommandInput = args.command.commandInputs.itemById(
#             'selIpt')

#         # 選択されている点のジオメトリのリストを作成
#         pnts = [selIpt.selection(idx).entity.worldGeometry for idx in range(
#             selIpt.selectionCount)]

#         # 球体ボディの作成
#         createSphereBodies(pnts)

def createSphereBodies(pnts: list):
    # 球体の半径 1Cm
    radius = 1

    # TemporaryBRepManagerの取得
    tmpMgr: adsk.fusion.TemporaryBRepManager = adsk.fusion.TemporaryBRepManager.get()

    # 球体の一時的なボディの作成 これは画面には表示されません
    sphereBodies: list = []
    pnt: adsk.core.Point3D
    for pnt in pnts:
        sphere: adsk.fusion.BRepBody = tmpMgr.createSphere(pnt, radius)
        sphereBodies.append(sphere)

    # ルートコンポーネントの取得
    app: adsk.core.Application = adsk.core.Application.get()
    des: adsk.fusion.Design = app.activeProduct
    root: adsk.fusion.Component = des.rootComponent

    # デザインが履歴付きかどうかを判断
    isParametric: bool = True
    if des.designType == adsk.fusion.DesignTypes.DirectDesignType:
        isParametric = False

    # 新しいコンポーネント(APIではOccurrence)の作成
    occ: adsk.fusion.Occurrence = root.occurrences.addNewComponent(
        adsk.core.Matrix3D.create()
    )

    # コンポーネントの取得
    comp: adsk.fusion.Component = occ.component

    # 履歴がある場合、ベースフューチャを作成する
    baseFeat: adsk.fusion.BaseFeature = None
    if isParametric:
        baseFeat = comp.features.baseFeatures.add()

    # ボディコレクションの取得
    bodies: adsk.fusion.BRepBodies = comp.bRepBodies

    # 球体をボディコレクションに追加する これで表示されます
    # 履歴の有無によって処理が異なる
    if isParametric:
        # パラメトリック
        baseFeat.startEdit()
        for body in sphereBodies:
            bodies.add(body, baseFeat)
        baseFeat.finishEdit()
    else:
        # ダイレクト
        for body in sphereBodies:
            bodies.add(body)
    
    # 無駄ですが5秒停止させます
    time.sleep(5)


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()

ちょっと書きたかった事を全てかけなかったので、次回も引き続き
SelectionCommandInputとイベントを扱おうと思っています。