C#ATIA

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

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

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

前回は、validateInputsとpreSelectイベントを取り扱いました。
個人的にはこの辺りのイベントを扱えば、ダイアログを使ったスクリプト
殆どこなせると感じています。
今回もSelectionCommandInputを複数配置した際の問題と回避方法を
扱いたいと思います。

今回のゴール

今回のテーマのSelectionCommandInputを2個配置した状態になります。
f:id:kandennti:20220126131644p:plain

それぞれリミットは1個づつとし、選択した2点を直径とする球体を作成する
コマンドを作成する事にします。
f:id:kandennti:20220126131657p:plain

ベースとなるコード

今回はイベントに関しては最低限にしています。
何度も呼び出す機会がある為、2個のSelectionCommandInputはグローバルな状態に
しました。
createSphereBetweenTwoPoints関数は記載していますが現在は使用していません。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

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

_handlers = []

# ダイアログの二個のSelectionCommandInput
_selIpt1: adsk.core.SelectionCommandInput = None
_selIpt2: adsk.core.SelectionCommandInput = None

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


# 2点間を直径とする球体の一時的なBRepBody作成
def createSphereBetweenTwoPoints(
    sktPnt1: adsk.fusion.SketchPoint,
    sktPnt2: adsk.fusion.SketchPoint) -> adsk.fusion.BRepBody:

    # ジオメトリの取得
    pnt1: adsk.core.Point3D = sktPnt1.worldGeometry
    pnt2: adsk.core.Point3D = sktPnt2.worldGeometry

    # 半径の取得
    radius: float = pnt1.distanceTo(pnt2) * 0.5
    if not radius > 0:
        return False # 同一点の場合はFalseとし処理中止

    # 中間点の取得
    centerPnt: adsk.core.Point3D = adsk.core.Point3D.create(
        (pnt1.x + pnt2.x) * 0.5,
        (pnt1.y + pnt2.y) * 0.5,
        (pnt1.z + pnt2.z) * 0.5,
    )

    # 球体作成
    tmpMgr: adsk.fusion.TemporaryBRepManager = adsk.fusion.TemporaryBRepManager.get()
    sphere: adsk.fusion.BRepBody = tmpMgr.createSphere(centerPnt, radius)

    return sphere


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)


            # inputs 今回はSelectionCommandInputをグローバルにしています。
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            global _selIpt1, _selIpt2
            _selIpt1 = inputs.addSelectionInput(
                'selIpt1',
                '1点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt1.addSelectionFilter('SketchPoints')

            _selIpt2 = inputs.addSelectionInput(
                'selIpt2',
                '2点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt2.addSelectionFilter('SketchPoints')

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


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を配置する弊害

スケッチを作成し点を幾つか描き、ベースのスクリプトを実行してみて下さい。
f:id:kandennti:20220126131716p:plain
画像ではわかりにくいのですが、1点目を選択した状態ですが、OKボタンが有効に
なってしまいます。本来であれば、選択すべきものが全て選択されたらOKボタンが
有効になって欲しいのですが、複数SelectionCommandInputを配置した際には
そのような動作をしてくれません。
と言う事は、前回行ったようにvalidateInputsイベントを利用してOKボタンを
制御する必要があります。

validateInputsイベントを実装する

今回はSelectionCommandInputをグローバルにしています。
スコープさえ理解していれば "global" を書かなくても意図した動きをしてくれる
事は分かっているのですが、後に深みに入らない為書くように心がけてますが、
どうでしょう?

まずはハンドラーです。
取りあえず1個づつ選択されている事と、同じ座標値の点の場合は球体が作成
出来ない為、直接createSphereBetweenTwoPoints関数で処理させ、OKボタンの
有効/無効を制御する事にしました。

class MyValidateInputsHandler(adsk.core.ValidateInputsEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.ValidateInputsEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # ダイアログ上のSelectionCommandInput
        global _selIpt1, _selIpt2

        # それぞれSelectionCommandInputが未選択時はFalse
        if _selIpt1.selectionCount < 1 or _selIpt2.selectionCount < 1:
            args.areInputsValid = False
            return

        # 選択された点で球体作成
        res = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 球体作成に失敗した際はOKボタンを押させない
        if not res:
            args.areInputsValid = False

続いて、ハンドラーの登録です。
念の為この位置に追記しますが、CommandCreatedハンドラーのnotifyメソッド内で
あれば、順番も関係ないです。

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

            # OKボタンの制御
            onValidateInputs = MyValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            _handlers.append(onValidateInputs)
・・・

これで同様にスクリプトを実行してください。
ん?1個だけ選択してもOKボタンが有効になってしまいます。
f:id:kandennti:20220126131731p:plain
そもそも、1個選択しただけではvalidateInputsイベントを呼び出してくれてません。
イベントは便利ですしイベントでないと処理しにくい部分があるのですが、
こういう時は困っちゃいますね。

不具合っぽさを解消する

この様な場合の対処方法を教わりました。
まず、SelectionCommandInputのリミットをデフォルトのままにすることが悪い
ようです。CommandCreatedハンドラーでリミットを "0" にします。

・・・
            global _selIpt1, _selIpt2
            _selIpt1 = inputs.addSelectionInput(
                'selIpt1',
                '1点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt1.addSelectionFilter('SketchPoints')
            _selIpt1.setSelectionLimits(0) # 追加 これ必要

            _selIpt2 = inputs.addSelectionInput(
                'selIpt2',
                '2点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt2.addSelectionFilter('SketchPoints')
            _selIpt2.setSelectionLimits(0) # 追加 これ必要
・・・

"setSelectionLimits(0)" は、幾つも点を選択出来るようになってしまします。
その為、リミットを制限するために前回利用したpreSelectイベントを利用
します。ハンドラーを作成します。

class MyPreSelectHandler(adsk.core.SelectionEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.SelectionEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # イベントが発生したSelectionCommandInput
        selIpt: adsk.core.SelectionCommandInput = args.activeInput

        # 既に選択済みなら、選択させない
        if selIpt.selectionCount > 0:
            args.isSelectable = False

そしてハンドラーの登録です。

・・・
            # リミットも制御する
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)

            # OKボタンの制御
            onValidateInputs = MyValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            _handlers.append(onValidateInputs)
・・・

再度スクリプトを実行してみます。
f:id:kandennti:20220126131840p:plain
今度は1個選択してもOKボタンは無効のままで、ValidateInputsイベント
(AreInputsValid)が発生しています。
発生しすぎのような気はしてますが。(バグのような気もしてますが・・・)

executeイベントを実装する

続いて実行結果を処理するexecuteイベントです。
ここはそれ程悩むことも無いと思います。

ハンドラーです。

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
        global _selIpt1, _selIpt2

        # 選択された点で一時的なBRepBody(球体)作成
        body: adsk.fusion.BRepBody = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 実体化
        self.createBody(body)

    # 一時的なBRepBodyを実体化
    def createBody(
        self,
        body: adsk.fusion.BRepBody):

        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

        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()
            bodies.add(body, baseFeat)
            baseFeat.finishEdit()
        else:
            bodies.add(body)

createSphereBetweenTwoPoints関数で得られるBRepBodyが一時的なBody
の為、画面には表示されません。その為、createBodyメソッドで表示する
ように処理しています。
(この部分は前回まで使用していた関数の一部を抜き出しただけです)

続いて、ハンドラーの登録です。

・・・
            # OKボタンを押した際のイベント
            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

            # リミットも制御する
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)
・・・

ここで実行してもらえると正しく動作すると思います。
但し、プレビューしませんね。

executePreviewを実装する

今まではexecuteとexecutePreviewが同じ処理にしていたため、
isValidResultプロパティを利用していました。
ダイアログなスクリプト入門4 - C#ATIA
今回は、カスタムグラフィックスを利用してプレビューさせることにします。
細かな事はドキュメントのこちらに説明があります。
Fusion 360 Help
かなり細かな事は出来ますが、結構面倒です。(そして一部バグがある)

ハンドラーです。

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
        global _selIpt1, _selIpt2

        # 選択された点で一時的なBRepBody(球体)作成
        body: adsk.fusion.BRepBody = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 球体作成に失敗した際はOKボタンを押させない
        if not body:
            args.isValidResult = True
            return

        # 球体を標示
        self.showPreviewBody(body)

    # 一時的なBRepBodyをカスタムグラフィックスで表示させる
    def showPreviewBody(self, body: adsk.fusion.BRepBody):
        # root 取得
        app: adsk.core.Application = adsk.core.Application.get()
        des: adsk.fusion.Design = app.activeProduct
        root: adsk.fusion.Component = des.rootComponent

        # カスタムグラフィックスで表示させる
        cgGroup: adsk.fusion.CustomGraphicsGroup = root.customGraphicsGroups.add()
        cgBody:adsk.fusion.CustomGraphicsBRepBody = cgGroup.addBRepBody(body)

        # 色の設定 いい加減です・・・。
        cgBody.color = adsk.fusion.CustomGraphicsBasicMaterialColorEffect.create(
            adsk.core.Color.create(0, 255, 0,255),
            adsk.core.Color.create(255, 255, 0, 255),
            adsk.core.Color.create(0, 0, 255, 255),
            adsk.core.Color.create(0, 0, 0, 255),
            10,
            0.5
        )

一時的なBodyは画面上に表示されない為、表示するだけのshowPreviewBodyメソッド
を作成しています。

続いてハンドラーの登録です。

・・・
            # OKボタンが有効時のイベント
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

            # OKボタンを押した際のイベント
            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)
・・・

トランザクション

過去の記述も含め、今までexecutePreviewイベント時の処理で作成しても一切削除を
行ってきませんでした。
例えば点を選択し直したり、選択を止めたりしても作成する処理だけ行っているだけ
です。今回作成しているカスタムグラフィックスも基本的に削除しなければならない
ものです。 ※カスタムグラフィックスはドキュメントに保存はされません。

作成したものを削除している正体は、トランザクションと言う仕組みで行われている
ようです。トランザクションについては、恐らくFusion360APIのドキュメントでは説明
されていないような気がしています。
Fusion360ではありませんが、こちらの説明が比較的分かりやすかったです。
「トランザクション」とは何か?を超わかりやすく語ってみた! - Qiita

executePreviewイベント時はトランザクション処理がされているようで、再表示する
前にイベント内で処理されたものは全て破棄されています。
又、今回の様にダイアログを使用したコマンド実行(CommandDefinition.execute)も
トランザクション処理されているようです。

例えば、前回までのものも含めOKボタンを押して終了させた場合、Undo歴に
コマンド名が追加されます。
f:id:kandennti:20220126131802p:plain
逆にキャンセルボタンを押して終了させた場合は、Undo歴には入りません。
ダイアログが表示されている間にプレビューさせていても、全ての処理が破棄
されています。

コマンドのイベントの中でもexecutePreviewイベントだけはトランザクション
処理されているようなので、逆に無理に作成したものを削除するような処理を
行ってしまうと思わぬものを削除してします可能性が有る為、注意しましょう。

フォーカスを制御する

ここまで行えば、それなりに動作するものになりました。
実際に実行して見て、少し不便に感じないでしょうか?
f:id:kandennti:20220126131817p:plain
1点目を選択後に続けて2点目を選択したいところなのですが、ダイアログの
SelectionCommandInputのフォーカスが1点目のままになっており、続けては
選択することが出来ません。画像は比較的ダイアログを近くに置いていますが、
離れた位置にある場合はダイアログまでマウスカーソルを持って行く事が結構な
手間です。ユーザーには不親切な設計ですね。

1点目を選択後は2点目にフォーカスを移動させたり、常に未選択の部分に
フォーカスが移動させると良さそうです。

ダイアログ上の変化を検知するのは、以前にも使用したinputChangedイベント
で可能です。これを利用しましょう。

ハンドラーはこの様にしました。

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

        # ダイアログ上のSelectionCommandInput
        global _selIpt1, _selIpt2

        # 一個目が未選択ならフォーカス
        if _selIpt1.selectionCount < 1:
            _selIpt1.hasFocus = True
            return

        # 二個目が未選択ならフォーカス
        if _selIpt2.selectionCount < 1:
            _selIpt2.hasFocus = True

SelectionCommandInputのフォーカスの有無は、hasFocusプロパティです。
Fusion 360 Help
単純に1個目・2個目の順番で、未選択だったらフォーカスさせているだけです。

続いてハンドラーの登録です。

・・・
            # SelectionCommandInputのフォーカスを制御したい
            onInputChanged = MyInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            _handlers.append(onInputChanged)

            # OKボタンが有効時のイベント
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)
・・・

1個目を選択後に続けて2個目も選択できますし、1個目を削除しても再度1個目の
再選択が続けて出来るようになりました。

まとめ

最終的な全てのコードはこの様になりました。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

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

_handlers = []

# ダイアログの二個のSelectionCommandInput
_selIpt1: adsk.core.SelectionCommandInput = None
_selIpt2: adsk.core.SelectionCommandInput = None

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


# 2点間を直径とする球体の一時的なBRepBody作成
def createSphereBetweenTwoPoints(
    sktPnt1: adsk.fusion.SketchPoint,
    sktPnt2: adsk.fusion.SketchPoint) -> adsk.fusion.BRepBody:

    # ジオメトリの取得
    pnt1: adsk.core.Point3D = sktPnt1.worldGeometry
    pnt2: adsk.core.Point3D = sktPnt2.worldGeometry

    # 半径の取得
    radius: float = pnt1.distanceTo(pnt2) * 0.5
    if not radius > 0:
        return False # 同一点の場合はFalseとし処理中止

    # 中間点の取得
    centerPnt: adsk.core.Point3D = adsk.core.Point3D.create(
        (pnt1.x + pnt2.x) * 0.5,
        (pnt1.y + pnt2.y) * 0.5,
        (pnt1.z + pnt2.z) * 0.5,
    )

    # 球体作成
    tmpMgr: adsk.fusion.TemporaryBRepManager = adsk.fusion.TemporaryBRepManager.get()
    sphere: adsk.fusion.BRepBody = tmpMgr.createSphere(centerPnt, radius)

    return sphere


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

        # ダイアログ上のSelectionCommandInput
        global _selIpt1, _selIpt2

        # 一個目が未選択ならフォーカス
        if _selIpt1.selectionCount < 1:
            _selIpt1.hasFocus = True
            return

        # 二個目が未選択ならフォーカス
        if _selIpt2.selectionCount < 1:
            _selIpt2.hasFocus = True


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
        global _selIpt1, _selIpt2

        # 選択された点で一時的なBRepBody(球体)作成
        body: adsk.fusion.BRepBody = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 球体作成に失敗した際はOKボタンを押させない
        if not body:
            args.isValidResult = True
            return

        # 球体を標示
        self.showPreviewBody(body)

    # 一時的なBRepBodyをカスタムグラフィックスで表示させる
    def showPreviewBody(self, body: adsk.fusion.BRepBody):
        # root 取得
        app: adsk.core.Application = adsk.core.Application.get()
        des: adsk.fusion.Design = app.activeProduct
        root: adsk.fusion.Component = des.rootComponent

        # カスタムグラフィックスで表示させる
        cgGroup: adsk.fusion.CustomGraphicsGroup = root.customGraphicsGroups.add()
        cgBody:adsk.fusion.CustomGraphicsBRepBody = cgGroup.addBRepBody(body)

        # 色の設定 いい加減です・・・。
        cgBody.color = adsk.fusion.CustomGraphicsBasicMaterialColorEffect.create(
            adsk.core.Color.create(0, 255, 0,255),
            adsk.core.Color.create(255, 255, 0, 255),
            adsk.core.Color.create(0, 0, 255, 255),
            adsk.core.Color.create(0, 0, 0, 255),
            10,
            0.5
        )


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
        global _selIpt1, _selIpt2

        # 選択された点で一時的なBRepBody(球体)作成
        body: adsk.fusion.BRepBody = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 実体化
        self.createBody(body)

    # 一時的なBRepBodyを実体化
    def createBody(
        self,
        body: adsk.fusion.BRepBody):

        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

        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()
            bodies.add(body, baseFeat)
            baseFeat.finishEdit()
        else:
            bodies.add(body)


class MyPreSelectHandler(adsk.core.SelectionEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.SelectionEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # イベントが発生したSelectionCommandInput
        selIpt: adsk.core.SelectionCommandInput = args.activeInput

        # 既に選択済みなら、選択させない
        if selIpt.selectionCount > 0:
            args.isSelectable = False


class MyValidateInputsHandler(adsk.core.ValidateInputsEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.ValidateInputsEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # ダイアログ上のSelectionCommandInput
        global _selIpt1, _selIpt2

        # それぞれSelectionCommandInputが未選択時はFalse
        if _selIpt1.selectionCount < 1 or _selIpt2.selectionCount < 1:
            args.areInputsValid = False
            return

        # 選択された点で球体作成
        res = createSphereBetweenTwoPoints(
            _selIpt1.selection(0).entity,
            _selIpt2.selection(0).entity,
        )

        # 球体作成に失敗した際はOKボタンを押させない
        if not res:
            args.areInputsValid = False


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)

            # SelectionCommandInputのフォーカスを制御したい
            onInputChanged = MyInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            _handlers.append(onInputChanged)

            # OKボタンが有効時のイベント
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

            # OKボタンを押した際のイベント
            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

            # リミットも制御する
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)

            # OKボタンの制御
            onValidateInputs = MyValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            _handlers.append(onValidateInputs)


            # inputs 今回はSelectionCommandInputをグローバルにしています。
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            global _selIpt1, _selIpt2
            _selIpt1 = inputs.addSelectionInput(
                'selIpt1',
                '1点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt1.addSelectionFilter('SketchPoints')
            _selIpt1.setSelectionLimits(0) # 追加 これ必要

            _selIpt2 = inputs.addSelectionInput(
                'selIpt2',
                '2点目を選択',
                'スケッチの点を選択して下さい'
            )
            _selIpt2.addSelectionFilter('SketchPoints')
            _selIpt2.setSelectionLimits(0) # 追加 これ必要

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


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を配置した際の実装と
対処方法です。SelectionCommandInputとそれに関するイベントについて
ばかり取り扱ってきましたが、実際ダイアログを利用したコマンドを作成
する場合は、利用する場面が多いはずです。

次回は・・・何を扱おうかな?