C#ATIA

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

Command Logger3

こちらの続きです。
Command Logger2 - C#ATIA

以前のものはこちらにUpしているのですが
Fusion360_Small_Tools_for_Developers/CommandLogger at master · kantoku-code/Fusion360_Small_Tools_for_Developers · GitHub
それとは別のお話。


APIフォーラムでのこちらに取り組んでみました。
CommandCreatedEvent not triggering for ApplicationCommands (such as Extrude, Fillet, ...) - Autodesk Community
単に使われたコマンドのログを取りたいのではなく、数値なども
取得したい との事。 ・・・個人的には出来ない事をグズグズ言ってても
しょうがないと思うのですが。

で、自分がアドバイスした方法でちょっと作りました。

# Fusion360API Python add-inns
import adsk.core, adsk.fusion, traceback
import re, threading, json

_app = None
_ui  = None
_handlers = []
_stopFlag = None
_myCustomEvent = 'MyCustomEventId'
_customEvent = None
_dialogInfos = []

class MyCommandTerminatedHandler(adsk.core.ApplicationCommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        blackList = [
            'SelectCommand',
            'CommitCommand'
        ]
        try:
            # command
            global _ui
            cmdId :str = args.commandId
            if cmdId in blackList:
                return

            cmdDef :adsk.core.CommandDefinition = _ui.commandDefinitions.itemById(cmdId)
            cmdName :str = cmdDef.name if cmdDef else '(unknown)'
            infos = [f'---{cmdName}---']

            # dialog
            global _dialogInfos
            infos.extend(_dialogInfos)

            dumpMsg('\n'.join(infos))

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

class ThreadEventHandler(adsk.core.CustomEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        def getDialogInfo() -> list:
            app = adsk.core.Application.get()

            txtCmdRes = app.executeTextCommand(u'Toolkit.cmdDialog')
            infos =[]
            for tmpInfo in txtCmdRes.split('MenuItems:\n')[:-1]:
                stateInfo = tmpInfo.split('\n')[-2]
                lst = [s.split('\n')[0] for s in stateInfo.split(',')]
                label = re.sub(r"\s+|Label=", "", lst[-2])
                value = re.sub(r"\s+", "", lst[-1])
                infos.append(f'{label}:{value}')
            
            app.executeTextCommand(u'Toolkit.hud')
            return infos

        try:
            global _dialogInfos
            _dialogInfos = getDialogInfo() #It's interfering with keyboard input.
        except:
            if _ui:
                _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

class MyThread(threading.Thread):
    def __init__(self, event):
        threading.Thread.__init__(self)
        self.stopped = event

    def run(self):
        global _app, _myCustomEvent
        while not self.stopped.wait(0.1):
            args = {'dmy': ''}
            _app.fireCustomEvent(_myCustomEvent, json.dumps(args)) 

def dumpMsg(msg :str):
    adsk.core.Application.get().userInterface.palettes.itemById('TextCommands').writeText(str(msg))

def run(context):
    try:
        dumpMsg('-- start add-ins --')

        global _app, _ui
        _app = adsk.core.Application.get()
        _ui = _app.userInterface

        global _myCustomEvent, _customEvent, _handlers
        _customEvent = _app.registerCustomEvent(_myCustomEvent)
        onThreadEvent = ThreadEventHandler()
        _customEvent.add(onThreadEvent)
        _handlers.append(onThreadEvent)

        global _stopFlag        
        _stopFlag = threading.Event()
        myThread = MyThread(_stopFlag)
        myThread.start()

        onCommandTerminated = MyCommandTerminatedHandler()
        _ui.commandTerminated.add(onCommandTerminated)
        _handlers.append(onCommandTerminated)

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

def stop(context):
    try:
        dumpMsg('-- stop add-ins --')

        global _handlers, _customEvent
        for h in _handlers:
            try:
                _ui.commandStarting.remove(h)
            except:
                pass
            try:
                _customEvent.remove.remove(h)
            except:
                pass

        global  _stopFlag, _app
        _stopFlag.set() 
        _app.unregisterCustomEvent(_myCustomEvent)

    except:
        if _ui:
            _ui.messageBox(_('AddIn Stop Failed: {}').format(traceback.format_exc()))

stop関数が非常に怪しい・・・。

コマンドのダイアログの入力値等を取得する為、テキストコマンドを
利用していますが、最初はCommandTerminatedイベントで処理させていたのですが、
CommandTerminatedイベントはダイアログが消えた直後にトリガー
されるようで、何も情報が取得出来ませんでした。

その為、カスタムイベントでスレッドを作成し、常にダイアログ情報を
取得し続けると言う荒業に切り替えた所、上手く情報を取得出来るように
なりました。

押し出しであればこんな感じです。
f:id:kandennti:20210423112135p:plain


おぉこれで十分じゃん! と思ったのですが、アドインを起動したまま
実際に操作してみると、キーボードからの入力が妨げられているようで、
数値等の入力が出来ないじゃないですか!(マウスでの操作は可能)

どうもスレッドでジャンジャンテキストコマンドを実行させている
事が原因なようで、色々試したものの改善しませんでした。

残念ながらボツです。

ボディ-平面間の最短距離

2個の要素の最短距離をAPIで知りたい事ってありますよね?

測定の為のオブジェクトはあります。こちら
Fusion 360 Help
これが実装されたのは2017年なんです。

その為、2016年に取り組んだこちらを取り組む際は苦労しました。
(細かな事は忘れてます)
スプリクトで2要素間の距離測定をしたい1 - C#ATIA

こちらも測定を必要としました。
閉空間球充填問題 - C#ATIA
こちらは2020年なのでMeasureManagerが有ったのですが、
ボディと平面の距離は測定出来ず、エラーとなります。
(平面がNGとはドキュメントには記載なし)

エラーの原因は平面が対応出来ていない事のように感じます。
そもそも、Fusion360の平面は無限な平面なのか疑問に感じる事もあり、
実装自体がやや望ましい形になっていないような気さえしてます。


最近になり、APIフォーラムでMeasureManagerでは平面を扱えない事に
対しての書き込みが有り、挑戦した結果がこちら。
Re: Body's distance to a ConstructionPlane - Autodesk Community
widthやlengthじゃなくてheightで良いはず・・・。

最初は、平面上に無駄なサーフェスを作成して測定してましたが、
ドキュメント内に無駄なものを作らずに測定出来ています。
恐らく、処理速度も速いはず。

カーソル上に工具を表示

Fusion360 Ver2.0.10032のこちら
Re: What's New 2021 - Autodesk Community

"カーソル上に工具を表示" (ctrlで検索するとHitするところ)の機能は、
以前アイデアに記載したものが実装されたようです。
マウスカーソル先端に工具のワイヤー表示 - Autodesk Community
ご丁寧に、ショートカットキーの CTRL+T まで一緒になってます。

技術的には難しくないと思うのですが、当時のDELCAMさんのアイデア
ナカナカ鋭いと思います。他のCAMソフトでは見たことが無いです。

これ便利だと思いますよ。僕は使ってないですけど・・・。
(PowerMillにはもっと良い機能があるので)

リンク付きAssyの作成

忘れちゃいそうなので覚書です。

こちらに取り組みました。
Solved: Betreff: API: change Parameter and save in current folder - Autodesk CommunitySolved: Betreff: API: change Parameter and save in current folder - Autodesk Community
取り組みたかったわけではないのですが、最初にレスしたので責任感じて。

上記リンク先の最初のレスは、特定のパラメータをAPIで編集しつつ異なる
ファイル名で保存したい と言うもので、これは別に問題無いです。

苦しんだのは2個目のレス。
個別に保存したファイルをコンポーネントとして取り込んだ状態にしたい
と解釈し挑戦しました。

データパネルのファイルの保存や読み込み自体は簡単なのですが、タイミングが
非常に面倒なんです。

こちらにファイルを開く処理は "こうじゃないのかな?" と書いたことがあります。
解決済み: Re: ファイルの修復ができるときとできない時がある - Autodesk Community解決済み: Re: ファイルの修復ができるときとできない時がある - Autodesk Community
今回は保存なのですが、こちらの逆の手順です。

ファイル保存されたイベントが、こちらに用意されているのですが
ほぼ役に立ちません。
Fusion 360 Help
恐らくこちらのイベントは "ファイル保存コマンド" が終了した際に発生するだけで
ファイル自体が保存された訳ではないんです。・・・困っちゃうんです。

そこで最初はDocumentのdataFileプロパティが取得出来れば、保存が完了している
事に気が付き、こんな感じの関数を作りました。

        def waitFinishedSaving(
            doc :adsk.core.Document):

                while True:
                    adsk.doEvents()
                    try:
                        doc.dataFile
                        break
                    except:
                        time.sleep(0.1)

dataFileプロパティが取得出来ない(保存が終わっていない)場合は、
例外で拾い出し、0.1秒後に再チャレンジしています。
でも、これ駄目なんです。 SaveAsは、既に古いdataFileが残った状態の為
保存が完了していなくても例外が出ないんです。

結果的に開いているドキュメントからdataFileの取得を諦め、クラウド側から
引っ張り込んでくることにしました。

        def getDataFileFromDataPanel(
            docNames :list,
            folder) -> list:

            wait = 3.0
            while True:
                try:
                    folder.dataFiles.asArray()
                    break
                except:
                    time.sleep(wait)
                    adsk.doEvents()

            names = []
            
            while True:
                for name in docNames:
                    if not name in names:
                        time.sleep(wait)
                        break
                else:
                    app.executeTextCommand('DataPanel.reload')                    
                    names = [f.name for f in folder.dataFiles.asArray()]
                    adsk.doEvents()
                    continue
                break

            dataFiles = folder.dataFiles.asArray()
            files = []
            for name in docNames:
                for df in dataFiles:
                    if name == df.name:
                        files.append(df)
                        break
                else:
                    continue

            return files

2回の無限ループと1回の多重ループと言うキモイ形になっちゃいました。

最初の "while True" では、DataFileオブジェクトにアクセスするだけです。
後からわかったのですが、どうもFusion360が保存している最中にクラウド側に
アクセスすると例外になるようです。
その為、例外を避ける為アクセスしているだけです。

2回目の "while True" では、欲しいDataFileが全て保存されているかどうかだけ
チェックしています。

最後の多重ループで目的のDataFileを取得しています。
・・・もうちょっと素直なコードに書き換えられそうな気がしているのですが


実はこれ実用的じゃない事例が2種類ある事に気が付いています。
・目的のフォルダ内に、目的のファイルと同一名のファイルがあると、目的では
 ない方のファイルを取得してしまう可能性が有る。
・個人ライセンスの場合、編集可能数10個に達した時点で保存が出来なくなるため
 あっさり無限ループ餌食になる。


まぁあのスクリプト自体が役立つような気がするので、まぁ良いかな。
リンクを切った状態のAssyであれば、ローカルキャッシュかエクスポートで
出来そうなので、もっと短時間で処理出来そうな気がする。

あなたのアカウント上の全ファイルリストを取得2

こちらの続きです。
あなたのアカウント上の全ファイルリストを取得 - C#ATIA

まさか続きを作るとは思っていませんでした。

データパネル内にデータファイルにアクセスするのが、非常に遅くて
使い物にならないと思っていたのですが、Fusion360 Ver2.0.10027で
アクセススピードが改善されるメソッドが追加されました。

※追記
チーム名も書き出すように修正しました。

# Fusion360API Python script GetDataList Ver0.0.4
# Author-kantoku
# Description-サインインしているID内の全ファイル名の取得

import adsk.core, adsk.fusion, adsk.cam, traceback
import time, datetime
from itertools import chain

def run(context):
    ui :adsk.core.UserInterface = None
    try:
        app :adsk.core.Application = adsk.core.Application.get()
        ui = app.userInterface
        
        # get export path
        path = get_Filepath(ui)
        if path is None:
            return
        
        # time
        t = time.time()
        

        # get data names
        names = []
        pros = app.data.dataProjects.asArray()

        if len(pros) < 1:
            return

        # init progress dialog
        progress :adsk.core.ProgressDialog = ui.createProgressDialog()
        progress.isCancelButtonShown = True
        progress.show('Export Files Name', '', 0, len(pros))

        # get files name
        for pro in pros:
            progress.progressValue += 1
            progress.message = f'Getting {pro.name} ....'
            adsk.doEvents()
            if progress.wasCancelled:
                return

            res = []
            names.append(getFiles(pro.rootFolder, res, 0))
        
        if len(names) < 1:
            ui.messageBox('Data not found')
            return
        
        # get info
        info = getInfo()

        # export
        info.extend(names)
        execExport(path, info)
        
        # finish
        ui.messageBox(f'Done:{time.time() - t}s')
        
    except:
        if ui:
            print('Failed:\n{}'.format(traceback.format_exc()))
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

# write file
def execExport(path, lst):
    file = open(path, 'w') 
    file.write('\n'.join(list(chain.from_iterable(lst))))
    file.close()

# User Name & Team Name & Date Time
def getInfo():
    app = adsk.core.Application.get()
    info = [
        [toDecode(f'User Name:{app.currentUser.displayName}')],
        [toDecode(f'Team Name:{app.data.activeHub.name}')],
        [f'Date Time:{datetime.datetime.now()}'],
        [' -- file name-- ']
        ]

    return info

# get file names
def getFiles(fld, res, depth):
    #folder
    res.append((' ' * (depth * 4)) + f'[{toDecode(fld.name)}]')

    #files
    fs = [(' ' * ((depth + 1) * 4)) + toDecode(f.name) for f in fld.dataFiles.asArray()]
    if len(fs) != 0:
        res.extend(fs)

    #subfolder
    fls = fld.dataFolders
    for subFld in fls:
        res = getFiles(subFld, res, depth + 1)

    return res

# decode
def toDecode(s):
     return s.encode("CP932", "ignore").decode("CP932")

# get file path
def get_Filepath(ui):
    dlg = ui.createFileDialog()
    dlg.title = 'File name export'
    dlg.isMultiSelectEnabled = False
    dlg.filter = 'text(*.txt)'
    if dlg.showSave() != adsk.core.DialogResults.DialogOK :
        return
    
    return dlg.filename

ものすごく早いわけではないのですが、以前があまりに遅かったので
かなり改善しています。(自分の場合、15~20秒ぐらい)

使い物になりそうなので、ご説明を。
f:id:kandennti:20210406153748p:plain
黄:ユーザー名と実行した際の日時
赤:一番左から書かれている"[ ]"内がプロジェクト名
緑:それ以外の"[ ]"内がフォルダ名
その他がドキュメント名です。

階層的にはスペース4文字で表現しています。

・・・ユーザー名部分のUser.displayNameが正しく
取得出来ない理由がわからん。
Fusion 360 Help

ES2016

あまり "入門" と付く書籍を読まなかったのですが、苦しくなり
図書館でこれを借りました。
https://www.amazon.co.jp/%E5%85%A5%E9%96%80JavaScript%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-JD-Isaacks/dp/479815864X

個人的にJavaScriptは闇が深すぎる言語のイメージなので、
reactを取り組んでいても謎だらけでしたが、少しだけ改善しつつあります。

一つ感じるのは、この手の書籍は著者が日本人のものより、
和訳本の方が内容的に優秀な気がしてます。
(そうではない本は、和訳されないのでしょうけど)

互換性を保とうとする言語は、黒歴史あった上なので闇が深い。