C#ATIA

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

SelectionCommandInputの不具合っぽさを解消する

こちらで質問しながら思いついた方法で、対処しました。
Solved: Operation using the SelectionCommandInput.addSelection method - Autodesk Community
不具合を再現するコードは、質問部分にあるものです。

ダイアログが表示される際に、addSelectionメソッドを使用して
事前選択した状態にしたいのです。
f:id:kandennti:20220417224108p:plain
これ自体は出来ています。問題はここで既に選択済みの要素を
クリックした際、通常のコマンドと同様に選択が解除される
事を期待していたのですが、CAD画面上は2個の選択された
状態に対して、ダイアログでは3個選択された状態となります。
f:id:kandennti:20220417230618p:plain


回避する為の方法は2個目の方ですが、InputChangedイベント時に
選択要素内に重複するものがあった場合、選択が解除された動作を
行ったものとして扱う事にすると、想定している動作をしました。
(要は不具合を回避出来ました)


ですが、最初の頃からCommandInputのイベント処理に違和感を
ずっと感じていたので、フォーラムのコードをちょっと修正しました。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

_app: adsk.core.Application = None
_ui: adsk.core.UserInterface = None
_handlers = []
_selIpt: adsk.core.SelectionCommandInput = None

class selectionInputBugSupportHandler(adsk.core.InputChangedEventHandler):
    def __init__(self, selInput: adsk.core.SelectionCommandInput):
        super().__init__()
        self.input = selInput

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

        if self.input != args.input:
            return

        ents = [self.input.selection(i).entity for i in range(self.input.selectionCount)]
        singles = [x for x in ents if ents.count(x) == 1]

        if len(ents) == len(singles):
            return

        self.input.clearSelection()
        [self.input.addSelection(x) for x in singles]


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)
            inputs: adsk.core.CommandInputs = cmd.commandInputs


            # inputs
            global _selIpt
            _selIpt = inputs.addSelectionInput(
                'selIptId',
                'sketch curves',
                'select sketch curves'
            )
            _selIpt.setSelectionLimits(0)
            _selIpt.addSelectionFilter(
                adsk.core.SelectionCommandInput.SketchCurves
            )

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

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

            onActivate = MyActivateHandler()
            cmd.activate.add(onActivate)
            _handlers.append(onActivate)

            onBugSupport = selectionInputBugSupportHandler(_selIpt)
            cmd.inputChanged.add(onBugSupport)
            _handlers.append(onBugSupport)


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

class MyActivateHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # Preselect sketch lines
        global _app, _selIpt
        des: adsk.fusion.Design = _app.activeProduct
        root: adsk.fusion.Component = des.rootComponent
        sktLines: adsk.fusion.SketchLines = root.sketches[0].sketchCurves.sketchLines
        _selIpt.clearSelection()
        _selIpt.addSelection(sktLines[0])
        _selIpt.addSelection(sktLines[1])


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)

        global _app, _selIpt
        selCount = _selIpt.selectionCount
        _app.log(f'selectionCount:{selCount}')
        for idx in range(selCount):
             _app.log(f' {_selIpt.selection(idx).entity.entityToken}')


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


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

        initSketchLines()

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

        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                'test_cmd',
                'Test',
                'Test'
            )

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


def initSketchLines():
    app: adsk.core.Application = adsk.core.Application.get()
    app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
    des: adsk.fusion.Design = app.activeProduct
    root: adsk.fusion.Component = des.rootComponent

    skt: adsk.fusion.Sketch = root.sketches.add(
        root.xYConstructionPlane
    )
    sktLines: adsk.fusion.SketchLines = skt.sketchCurves.sketchLines

    P3D = adsk.core.Point3D
    points =[
        (P3D.create(1,0,0), P3D.create(1,2,0)),
        (P3D.create(2,0,0), P3D.create(2,2,0)),
        (P3D.create(3,0,0), P3D.create(3,2,0)),
        (P3D.create(4,0,0), P3D.create(4,2,0)),
    ]
    for p1, p2 in points:
        sktLines.addByTwoPoints(p1, p2)

    app.activeViewport.fit()

細かな説明を省きます(今日は疲れて眠い・・・)が、満足。

リストからリストを除去する

"リスト" と言う文字から "リスト" を除去すると何も残りません。
そう言う事じゃないです。

pythonのリストから特定の1要素を除去する場合、幾つか方法があるようです。
Pythonでリスト(配列)の要素を削除するclear, pop, remove, del | note.nkmk.me

1要素では無くて、リストに入っている要素全てを除去する
清楚な書き方はどんなのがあるのかな?
と迷いました。

具体的に書けば、

ターゲットリストは0~9
除去したいリストは3,6,9

結果としては0, 1, 2, 4, 5, 7, 8が欲しい ってことです。

"3の倍数ならさっきのリンク先の一番最後の内包表記でif使えば簡単じゃん"
とお思いのあなた、そうです。
但し 偶々除去したいリストが偶々3の倍数だっただけです。

forでグリグリ廻せば出来ますが、それは清楚じゃないです。(下らないこだわり)
あくまでリストからリストを除去する清楚な書き方としたいんです。

removeの引数がリストでもイケるかな? と思ったのですが、
ダメなんですね。ん~。


結局思い付いたのがこちら。

import traceback
import adsk.fusion
import adsk.core

def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface

        targetList: list = list(range(10))
        removeList: list = [3,6,9]

        app.log(f'targetList:{targetList}')
        app.log(f'removeList:{removeList}')

        # remove 
        # resList = targetList.remove(removeList) # NG

        resList = [v for v in targetList if not v in removeList]
        app.log(f'resList:{resList}')

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

もう、Fusion360とは無関係ですが出力の関係でimportしてます。

結果はこちら

 targetList:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 removeList:[3, 6, 9]
 resList:[0, 1, 2, 4, 5, 7, 8]

結局、内包表記のifです・・・。
1行にforとifとinが2回が入っているのは、他言語の方は異様に感じると思いますね。

数値では無く、あくまでオブジェクトでも大丈夫でした。(絶対忘れる)

トリム前後で同一の線と判断したい2

こちらの続きです。
トリム前後で同一の線と判断したい1 - C#ATIA

早速、属性が引き継がれるんじゃないかな?テストコード。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface
        des: adsk.fusion.Design = app.activeProduct
        root: adsk.fusion.Component = des.rootComponent

        skt: adsk.fusion.Sketch = root.sketches.add(
            root.xYConstructionPlane
        )
        circles: adsk.fusion.SketchCircles = skt.sketchCurves.sketchCircles
        
        cir1: adsk.fusion.SketchCircle = circles.addByCenterRadius(
            adsk.core.Point3D.create(1,1,0),
            2.0
        )
        cir1.attributes.add(
            'testGroup',
            'hoge',
            'piyo'
        )

        cir2: adsk.fusion.SketchCircle = circles.addByCenterRadius(
            adsk.core.Point3D.create(2,2,0),
            1.0
        )

        app.log('TextCommandWindow.Clear')
        dumpInfo(cir1)

        tools: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
        tools.add(cir2)

        res = cir1.trim(
            adsk.core.Point3D.create(3,3,0),
        )
        dumpInfo(cir1)

        ark1 = res.item(0)
        dumpInfo(ark1)

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


def dumpInfo(sktEnt: adsk.fusion.SketchEntity) -> None:

    def isVisible(sktEnt: adsk.fusion.SketchEntity) -> bool:
        try:
            if not hasattr(sktEnt, 'isVisible'):
                return False

            return sktEnt.isVisible
        except:
            return False

    def getToken(sktEnt: adsk.fusion.SketchEntity) -> str:
        try:
            return sktEnt.entityToken
        except:
            return ''

    def getAttrsStr(ent) -> str:
        try:
            if not hasattr(ent, 'attributes'):
                return ''
            
            attrs: adsk.core.Attributes = ent.attributes
            if attrs.count < 1:
                return ''

            attr: adsk.core.Attribute
            lst = []
            for gpName in attrs.groupNames:
                lst.append(f'  GroupName : {gpName}')
                for attr in attrs.itemsByGroup(gpName):
                    lst.append(f'    {attr.name}:{attr.value}')

            return '\n'.join(lst)
        except:
            return ''

    # *********
    msg = '-------\n'
    msg += f'classType | {sktEnt.classType()}\n'
    msg += f'isVisible | {isVisible(sktEnt)}\n'
    msg += f'token | {getToken(sktEnt)}\n'
    msg += f'attrs | \n{getAttrsStr(sktEnt)}\n'

    adsk.core.Application.get().log(msg)

結果はこちら。

 -------
classType | adsk::fusion::SketchCircle
isVisible | True
token | /v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZQpZUsctTXQ/5Tb+ly/8sv1sFVGGkYGymYG5qkWJikGaua5pikqZrYmBpomtpYZCka5RiYWZmYmhmlGpgAlYLNIMBDYDtM1AAW2dqaASx1hyo1ADIBgChA1jrkgAAAA==
attrs | 
  GroupName : testGroup
    hoge:piyo

 -------
classType | adsk::fusion::SketchCircle
isVisible | False
token | 
attrs | 


 -------
classType | adsk::fusion::SketchArc
isVisible | True
token | /v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZQpZUsctTXQ/5Tb+ly/8sv1sFVGGkYGymYG5qkWJikGaua5pikqZrYmBpomtpYZCka5RiYWZmYmhmlGpgAlYLNIMBDYDtM1AAW2dqaASx1hyo1ACELQDvwUqZlAAAAA==
attrs | 

ふぅ、結論無理。

トリム前後で同一の線と判断したい1

わかりにくいタイトルです。

要はこんなお話です。
f:id:kandennti:20220408162903p:plain
左の様に円を2個描きます。その後、小さい円を利用して、
大きい円をトリムした際、トリム前の大きな円が
トリム後の円弧になっている事を知りたいんです。

GUIで作業している分には円(閉じている)と円弧(開いている)
がトリムによって変わったことは一目瞭然なのですが、
APIだと難しいです。(人間の目と脳は素晴らしい)
何故なら、円と円弧ではオブジェクトが異なるからです。
Fusion 360 Help
Fusion 360 Help
・・・メンドクサイ。

トリム自体をAPIで行うのであれば、見失う事は無いのですが、
GUIでトリムされたものをAPIで見つけ出すのが難しい。

とりあえずサンプルです。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface
        des: adsk.fusion.Design = app.activeProduct
        root: adsk.fusion.Component = des.rootComponent

        skt: adsk.fusion.Sketch = root.sketches.add(
            root.xYConstructionPlane
        )
        circles: adsk.fusion.SketchCircles = skt.sketchCurves.sketchCircles
        
        cir1: adsk.fusion.SketchCircle = circles.addByCenterRadius(
            adsk.core.Point3D.create(1,1,0),
            2.0
        )
        cir2: adsk.fusion.SketchCircle = circles.addByCenterRadius(
            adsk.core.Point3D.create(2,2,0),
            1.0
        )

        dumpInfo(cir1)

        tools: adsk.core.ObjectCollection = adsk.core.ObjectCollection.create()
        tools.add(cir2)

        res = cir1.trim(
            adsk.core.Point3D.create(3,3,0),
        )
        dumpInfo(cir1)

        ark1 = res.item(0)
        dumpInfo(ark1)

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


def dumpInfo(sktEnt: adsk.fusion.SketchEntity) -> None:

    def isVisible(sktEnt: adsk.fusion.SketchEntity) -> bool:
        try:
            if not hasattr(sktEnt, 'isVisible'):
                return False

            return sktEnt.isVisible
        except:
            return False

    def getToken(sktEnt: adsk.fusion.SketchEntity) -> str:
        try:
            return sktEnt.entityToken
        except:
            return ''

    # *********
    msg = '-------\n'
    msg += f'classType | {sktEnt.classType()}\n'
    msg += f'isVisible | {isVisible(sktEnt)}\n'
    msg += f'token | {getToken(sktEnt)}'

    adsk.core.Application.get().log(msg)

entityTokenは、それぞれ固有の要素を示すIDのようなもの(ハッシュ)
です。その為、実行する度に代わってしまいますがその一例の結果です。

 -------
classType | adsk::fusion::SketchCircle
isVisible | True
token | /v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwj4uq0Xvt7r6rXnWEJLRzsgNVGGkYGymYGmeZmhhYJSsa5ZqZqprYpFkpptolmiga2ppmJJmZmaRaGxgAlYLNIMBDYDtM1AAW2dqaASx1hyo1ADIBgDODUKvkgAAAA==
 -------
classType | adsk::fusion::SketchCircle
isVisible | False
token | 
 -------
classType | adsk::fusion::SketchArc
isVisible | True
token | /v4BAAAARlJLZXkAH4sIAAAAAAAA/zNQsFAwAEJDBSMgBrOMLIAsQwUTkIiZwj4uq0Xvt7r6rXnWEJLRzsgNVGGkYGymYGmeZmhhYJSsa5ZqZqprYpFkpptolmiga2ppmJJmZmaRaGxgAlYLNIMBDYDtM1AAW2dqaASx1hyo1ACELQCa35eclAAAAA==

大きい円(cir1)はトリム後に見失っています。(isVisible | False)
大きい円(cri1)とトリム後の円弧(ark1)のentityTokenは似ているのですが、
ここから判断するのは無理・・・。
そもそもentityTokenの文字列に意味を求めても無駄だと、ドキュメントに
記載されていた記憶があります。

諦めようかとも思っていたのですが、書いているうちに属性は引き継いで
くれそうな予感がしてきました。
近いうちに試そう。(3度目のワクチンから回復したら・・・)

OneDriveの同期

MicrosoftのOneDriveを利用してFusion360のアドイン/スクリプト
職場と自宅で共用しているのですが、同期が上手く行かない事が
シバシバ。
過去に同期していない事に気が付かず、うっかり作りかけの物を
上書きしてしまい、涙した事もあります。そんなにでも無いけど。

検索すれば出てくると思うのですが、僕がやっている強制的な
同期方法です。

まず、OneDriveを止める!
f:id:kandennti:20220408111127p:plain

続いてOneDriveを再起動!
f:id:kandennti:20220408111256p:plain

面倒だなんて思ってません。大好きですよMicrosoft
GoogleドライブとかDropboxとかには浮気しないよ。
特にGoogleドライブはUpしたデータの権限が怪しい。

開発者向けアドイン10

こちらの続きです。
開発者向けアドイン9 - C#ATIA

スクリプトでは不便すぎるので、アドインに2コマンド追加しました。
Fusion360DevTools - C#ATIA
f:id:kandennti:20220407174557p:plain

Fusion360_Small_Tools_for_Developers/Developers_Small_ToolKit at master · kantoku-code/Fusion360_Small_Tools_for_Developers · GitHub


あぁつくづく単体のリポジトリすれば良かった・・・。

Fusion360DevTools

Fusion360DevToolsと言う開発者向けの便利なツールがgithub
公開されているのです。
GitHub - AutodeskFusion360/Fusion360DevTools: A collection of utilities to assist in developing Fusion 360 Add-ins

便利なんですが、中には使った後にFusion360がクラッシュするコマンドが
ありました。

ちょっとドキュメントの属性を利用したいのでテストしているのですが、
属性はユーザーが見ることが出来ない事がありがたい反面、開発中に
確認出来ないと言うもどかしさがあるのですが、上記のアドインの
こちらが利用出来て便利なはずなんです。
f:id:kandennti:20220407164655p:plain

ところが全く無反応・・・、Update前は動いていたような記憶は
あるのですがダメです。

確認出来ないと話にならないので作ります・・・作りました。

アクティブドキュメントの属性をテキストコマンドウィンドウに全てダンプ。

# Dump Document Attributes
# Fusion360API Python script

import traceback
import adsk.cam
import adsk.fusion
import adsk.core
import adsk.drawing

def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface

        doc: adsk.fusion.FusionDocument = app.activeDocument
        attrs: adsk.core.Attributes = doc.attributes

        app.log('TextCommandWindow.Clear')
        app.log('-- Dump Document Attributes --')
        attr: adsk.core.Attribute
        for gpName in attrs.groupNames:
            app.log(f'GroupName : {gpName}')
            for attr in attrs.itemsByGroup(gpName):
                app.log(f'  {attr.name}:{attr.value}')

作る事がAPIでしか出来ないのですが、削除もAPIからしか出来ません。
ドキュメントの属性を全部削除するスクリプト

# Remove Document Attributes
# Fusion360API Python script

import traceback
import adsk.cam
import adsk.fusion
import adsk.core
import adsk.drawing

def run(context):
    ui: adsk.core.UserInterface = None
    try:
        app: adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface

        doc: adsk.fusion.FusionDocument = app.activeDocument
        attrs: adsk.core.Attributes = doc.attributes

        app.log('TextCommandWindow.Clear')
        app.log('-- Remove Document Attributes --')
        app.log(f'before count:{attrs.count}')
        attr: adsk.core.Attribute
        for gpName in attrs.groupNames:
            app.log(f'GroupName : {gpName}')
            for attr in attrs.itemsByGroup(gpName):
                attr.deleteMe()
        app.log(f'after count:{attrs.count}')

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

中々進まない・・・。