C#ATIA

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

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

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

前回は、executeとexecutePreviewイベントを取り扱いました。相変わらず役立つ
物ではありませんが、目の前に結果が表示されるようになると ”やっている” 感は
感じると思います。
今回は少し地味目なテーマですが、より細かな選択のフィルターとOKボタンの制御
を扱いたいと思います。

今回のゴール

見た目は前回と同じSelectionCommandInputが1個だけです。
f:id:kandennti:20220124112136p:plain

あまり現実的ではありませんが、今回のフィルターとしての条件は
・スケッチの点でY座標がマイナスの場合、選択させない。

又、OKボタンを利用出来るようにするためには
・3点以上の点が選択され、1個以上はX座標がプラスの必要がある。

とします。無理やりですね・・・。

ベースとなるコード

こちらは前回の最終的なものとほぼ同じです。
但し、SelectionCommandInput.setSelectionLimitsメソッドを設定して
おりません。

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

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

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

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)

        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

def createSphereBodies(pnts: list):
    radius = 1

    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

    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)


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

より細かなフィルターを作る

選択フィルターについては以前こちらで扱いました。
ダイアログなスクリプト入門2 - C#ATIA

フィルターについては大半は用意されているもので事が足りますが、
より細かな条件のものが必要な場合になる事もあると思います。
それを実現するのが事前選択になります。
事前選択を別の表現をすれば、選択可能な要素の上にマウスカーソルを持って
行った際、マウスカーソル下の要素を取得する事が可能です。

Fusion360APIでは2種類用意されており、"preSelect" と "preSelectMouseMove" が
有ります。 ・・・個人的にドキュメントの説明からするとイベントの発火が
逆のような気がしてます。

どちらでも同じですが、今回はpreSelectイベントを利用する事にしましょう。
まず、CommandCreatedイベントハンドラー内に追加します。
イベントハンドラーの登録はCommandCreatedイベントハンドラー内だと思って
おいて大丈夫です。

・・・
            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

            # preSelectイベントハンドラー登録
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)

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

続いてハンドラーを作成します。

# 事前選択です!
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)

        # マウスカーソルの下の要素の選択
        sel: adsk.core.Selection = args.selection
        sktPnt: adsk.fusion.SketchPoint = sel.entity

        # ジオメトリの取得
        pnt: adsk.core.Point3D = sktPnt.worldGeometry

        # 条件で選択を判断する
        if pnt.y < 0:
            # Y座標がマイナスの場合、選択を許さない
            args.isSelectable = False

"args.selection" は、SelectionCommandInput.selectionと同じ型のオブジェクトに
なります。
Fusion 360 Help
カーソル下の実際の要素は args.selection.entity となる為、注意が必要です。

"条件で選択を判断する" で座標値を判断していますが、args.isSelectable
プロパティで選択可能か?不可能か?を設定する事が出来ます。
これがフィルターとして機能します。

args.isSelectableはデフォルトがTrue(選択可能)として受け取っているので、
選択可の場合は態々Trueにする必要も無いでしょう。

又、SelectionCommandInputのaddSelectionFilterでフィルターを設定している場合、
preSelectイベントは発生しません。
(今回の場合であれば、ボディの面の上にマウスカーソルを持って行っても
イベントは発生しません。)


より細かなフィルターを体感する

では、XY平面上にスケッチを作成し、原点を中心に幾つかの点を作成して
実行してみて下さい。
f:id:kandennti:20220124112832p:plain
赤がX軸、緑がY軸です。
先程の条件からして、青の領域部分は選択可能で、赤の領域部分は選択不可能に
なっている事が確認出来ると思います。

今回の条件が非現実的なのですが、より細かなフィルターは事前選択で
作り出すことが可能と言う事になります。

但し、1個選択した時点でプレビューは表示されていますし、OKボタンが押せる
状態です。OKボタンを制御する必要がありますね。

OKボタンを制御する

OKボタンを有効にする為の条件は
"3点以上の点が選択され、1個以上はX座標がプラスの必要がある。"
としました。要は
f:id:kandennti:20220124113200p:plain
青領域の中の3個以上でかつ、紫領域から1個以上選択されている状態で
OKボタンを押せるようにします。無理やりな条件ですね。

OKボタンの制御は、validateInputsイベントで行います。
Fusion 360 Help
逆の表現をすれば、validateInputsイベントでしか行えません。

ドキュメント通りにイベントハンドラを登録します。

・・・
            # preSelectイベントハンドラー登録
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)

            # validateInputsイベントハンドラー登録
            onValidateInputs = MyValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            _handlers.append(onValidateInputs)
・・・

続いてハンドラーです。

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)

        # ダイアログ上の全てのcommand inputsを取得
        inputs: adsk.core.CommandInputs = args.inputs

        # IDでSelectionCommandInputを取得
        selIpt: adsk.core.SelectionCommandInput = inputs.itemById('selIpt')

        # 選択数が3個未満の場合はOKボタンを抑制する。
        selCount: int = selIpt.selectionCount
        if selCount < 3:
            args.areInputsValid = False
            return

        # 選択された全てのスケッチ点取得
        sktPnts = [selIpt.selection(idx).entity for idx in range(selCount)]

        # X座標がプラスの物のみの点を取得
        p: adsk.fusion.SketchPoint
        xPulsPnts = [p for p in sktPnts if not p.worldGeometry.x < 0]

        # X座標プラスの点が1個も無い場合はOKボタンを抑制する。
        if len(xPulsPnts) < 1:
            args.areInputsValid = False

adsk.core.ValidateInputsEventArgs.areInputsValidがOKボタンの有効/無効を
管理しているプロパティです。
Fusion 360 Help
再度記載しますが、このプロパティでしか制御出来ません。

必要条件を満たしているかどうかをチェックし、満たしていない場合はFalseに
します。デフォルト値はTrueの為、条件を満たしている場合は特に何かをする必要は
無いでしょう。

これで実際にスクリプトを実行して頂くと、最初に提案した非現実的な条件を満たす
までは、プレビューもされずOKボタンを押せない状態になりました。

まとめ

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

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

            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

            # preSelectイベントハンドラー登録
            onPreSelect = MyPreSelectHandler()
            cmd.preSelect.add(onPreSelect)
            _handlers.append(onPreSelect)

            # validateInputsイベントハンドラー登録
            onValidateInputs = MyValidateInputsHandler()
            cmd.validateInputs.add(onValidateInputs)
            _handlers.append(onValidateInputs)


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

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

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

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)

        # ダイアログ上の全てのcommand inputsを取得
        inputs: adsk.core.CommandInputs = args.inputs

        # IDでSelectionCommandInputを取得
        selIpt: adsk.core.SelectionCommandInput = inputs.itemById('selIpt')

        # 選択数が3個未満の場合はOKボタンを抑制する。
        selCount: int = selIpt.selectionCount
        if selCount < 3:
            args.areInputsValid = False
            return

        # 選択された全てのスケッチ点取得
        sktPnts = [selIpt.selection(idx).entity for idx in range(selCount)]

        # X座標がプラスの物のみの点を取得
        p: adsk.fusion.SketchPoint
        xPulsPnts = [p for p in sktPnts if not p.worldGeometry.x < 0]

        # X座標プラスの点が1個も無い場合はOKボタンを抑制する。
        if len(xPulsPnts) < 1:
            args.areInputsValid = False


# 事前選択です!
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)

        # マウスカーソルの下の要素の選択
        sel: adsk.core.Selection = args.selection
        sktPnt: adsk.fusion.SketchPoint = sel.entity

        # ジオメトリの取得
        pnt: adsk.core.Point3D = sktPnt.worldGeometry

        # 条件で選択を判断する
        if pnt.y < 0:
            # Y座標がマイナスの場合、選択を許さない
            args.isSelectable = False


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)

        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

def createSphereBodies(pnts: list):
    radius = 1

    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

    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)


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

OKボタンを制御する為には "ValidateInputs" イベントを使用する事は、
知られているのですが、より細かな選択フィルターを作る為には
"preSelect" 又は "preSelectMouseMove" を使用する方法はあまり
知られていないような印象が有ります。

次回も引き続きSelectionCommandInputを扱います。実はバグでは無いかと
思う部分が有る為、それを避ける為の方法をご紹介します。