C#ATIA

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

その場凌ぎ

いかん、更新していませんでした。

特にネタも無いのも事実ですが、比較的最近APIフォーラムに
投げたものをご紹介。
(ブログには書いて無いのですが、ソコソコやってます)

・Web上のf3dファイルをインポートする
Solved: How to import file into Fusion 360 from web-server using Import Manager? - Autodesk Community
クラウドではなく、何処かにUpされているf3dファイルをダウンロード
してFusion360にインポートします。出来るんですね。

その後続きがこちらになってます。
How to link an add-in application with web-server? - Autodesk Community
インポートしているテキストコマンド部分が、単体の時は問題無かった
のですが、今回の形ではFusion360ではクラッシュしました。
散々探した所、

Fusion.ImportComponent /NoMove <ファイルパス>

であればクラッシュしなくなりました。


・更新を止めつつパラメータの値の変更
Solved: Parameter I/O Add-in Question - Autodesk Community
1個パラメータ(要は寸法値など)を変更する度に、3Dの方が
更新されるので処理が遅く、全てのパラメータを変更した後に
3Dを更新するように出来ないか? と言うような内容です。

CATIAのマクロの場合は、数値を変更してもUpdateするまで
基本的には止まった状態になっているのですが、Fusion360
場合はドンドン更新しちゃうんです。
最初は更新止める為のテキストコマンドを探したのですが、
見つからず諦めかけたのですが、タイムラインを一番最初に
戻すしてもパラメータ類は残ったままになる事に気が付き、
それをコードにしました。


・設定値付きのコマンドのログ
CommandCreatedEvent not triggering for ApplicationCommands (such as Extrude, Fillet, ...) - Autodesk Community
不完全なこちらです。
Command Logger3 - C#ATIA


・大量の断面作成
Export surface from plane cut - Autodesk Community
最初にTemporaryBRepManager使った方法を作ったのですが、
質問者さんが満足せず、履歴付きの物を再投稿。
処理時間が遅すぎて、僕なら嫌なんだけどなぁ。


他にもチョロチョロ作ったんですが、目新しいものは無いかも。

Options.Get

テキストコマンドを利用して、現状の状態を取得する方法の一つに

Options.Get

があります。
Fusion360_Small_Tools_for_Developers/TextCommands_txt_Ver2_0_8176.txt at master · kantoku-code/Fusion360_Small_Tools_for_Developers · GitHub

例えば、こちらで僕の質問に対して回答をもらっています。
Solved: Re: Get the user name - Autodesk Community

実はこのコマンド、自分は見付けられていませんでした。
そもそも引数となるが何なのか? 全くわかりませんでした。
何処かにあるXMLJsonのファイルから探し出すのかな? と思っていたの
ですが、見つける事が出来ず諦めていました。

何となく、コマンド類を見直した所、やっと見つけました。

Options.GetOptions /full

Fusion360_Small_Tools_for_Developers/TextCommands_txt_Ver2_0_8176.txt at master · kantoku-code/Fusion360_Small_Tools_for_Developers · GitHub
これで良さそうです。

また地味知識が身についた。
念の為、こちらの基本設定より細かな設定や設定情報の取得が出来そう。
f:id:kandennti:20210429101845p:plain

EAGLE

PCB 設計・電気回路図ソフトのEAGLEは、Fusion360と統合されていると
思うのですが、単体でも残っているのかな?


偶々気が付いたのですが、こちらのフォルダ(win10です)はFusion360
インストールした際に出来るパスです。

C:\Users\<ユーザー名>\AppData\Local\Autodesk\webdeploy\production\c7fc77995ea95f590289dcbcdcd1b4a6144b515c\Applications\Electron\LibEagle

復活の呪文的なフォルダはバージョン毎に切り替わるフォルダです。

このフォルダ内に ”eagle.exe” が有りました。
f:id:kandennti:20210428094117p:plain

起動してみると、サインインのダイアログが出現するので
Fusion360と同じように入力すると・・・起動しました。
f:id:kandennti:20210428094741p:plain
EAGLE自体が分かっていない為、これが本当にEAGLEなのか?が
分かていないのですが、恐らくEAGLEなのだろうと思います。


Autodeskアカウントで確認すると
f:id:kandennti:20210428094459p:plain
EAGLEのライセンスも入っていました。
ん?以前はHSMのライセンスも入っていたような記憶が
有ったのですが、無いですね。

MeshBodyをBRepBodyに変換1

先日のドロップダウンリストが出来るようになったので、
今回は副産物を作る事にしました。
ドロップダウンリストを変更する - C#ATIA

個人的には、STL(だけではなくOBJ等も)データをソリッド/サーフェス
に変換しても、重たいだけで役には立たないと思っているのですが、
世の中には需要があるのも確かです。こちらもそうですね。
Convert Mesh to BRep in API - Autodesk Community

Fusion360にはその為のコマンドが用意されているものの、50000枚以下
と制限が有ります。・・・その処理を必要とする場合は恐らく50000枚は
軽く超えているハズです。

その為、変換可能なサイズまでぶった切り、複数個にして変換する為の
スクリプトを作成しました。

# Fusion360API Python script
# Author-kantoku
# Description-Meshをぶった切りつつBRep化

import adsk.core, adsk.fusion, traceback

_app = adsk.core.Application.cast(None)
_ui = adsk.core.UserInterface.cast(None)

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

        if des.designType == adsk.fusion.DesignTypes.ParametricDesignType:
            query = _ui.messageBox(
                'ダイレクトモードに切り替えます。\nよろしいですか?',
                'ダイレクトモードに切り替える必要があります',
                adsk.core.MessageBoxButtonTypes.OKCancelButtonType,
                adsk.core.MessageBoxIconTypes.QuestionIconType)

            if query == adsk.core.DialogResults.DialogCancel:
                return
            else:
                des.designType = adsk.fusion.DesignTypes.DirectDesignType

        msg :str = 'メッシュボディを選択して下さい!'
        selFiltter :str = 'MeshBodies'
        sel :adsk.core.Selection = selectEnt(msg ,selFiltter)
        if not sel: return
        mesh = sel.entity

        fact = Mesh2BRepFactry(mesh)
        bodies = fact.toBrep()

        _ui.messageBox(f'{len(bodies)}個のBRepボディを作成しました')

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

def selectEnt(
    msg :str, 
    filtterStr :str
    ) -> adsk.core.Selection :

    try:
        sel = _ui.selectEntity(msg, filtterStr)
        return sel
    except:
        return None

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



class Mesh2BRepFactry:
    def __init__(self, mesh :adsk.fusion.MeshBody):
        self.mesh = mesh

    def toBrep(self, maxCount = 10000) -> list:
        comp :adsk.fusion.Component = self.mesh.parentComponent

        if maxCount > 10000:
            maxCount = 10000

        # split
        meshLst = self._MeshsSplit(self.mesh, maxCount)

        # toBrep
        return self._Mesh2BRepCommand(meshLst)


    def _Mesh2BRepCommand(
        self,
        meshLst :list) -> list:

        app :adsk.core.Application = adsk.core.Application.get()
        ui :adsk.core.UserInterface = app.userInterface
        sels :adsk.core.Selections = ui.activeSelections
        comp :adsk.fusion.Component = self.mesh.parentComponent
        exceptionLst = [b for b in comp.bRepBodies]

        for mesh in meshLst:
            sels.clear()
            sels.add(mesh)
            app.executeTextCommand(u'Commands.Start Mesh2BRepCommand')
            app.executeTextCommand(u'NuCommands.CommitCmd')

            adsk.doEvents()
        
        return self._list_difference([b for b in comp.bRepBodies], exceptionLst)


    def _MeshsSplit(
        self,
        mesh :adsk.fusion.MeshBody,
        maxCount) -> list:

        comp :adsk.fusion.Component = self.mesh.parentComponent

        meshLst = [self.mesh]

        exceptionLst = [m for m in comp.meshBodies]
        exceptionLst.remove(self.mesh)

        reOpeFG = False
        while True:
            for mesh in meshLst:
                dumpMsg(f'{mesh.name}:{mesh.displayMesh.triangleCount}')
                if maxCount < mesh.displayMesh.triangleCount:
                    tool :adsk.fusion.ConstructionPlane = self._getSplitPlane(mesh)
                    self._MeshPlaneCutCommand(mesh, tool)
                    tool.deleteMe()
                    reOpeFG = True

                adsk.doEvents()
            
            if reOpeFG:
                meshLst = self._list_difference([m for m in comp.meshBodies], exceptionLst)
                reOpeFG = False
            else:
                break
        
        return meshLst


    def _MeshPlaneCutCommand(
        self,
        mesh :adsk.fusion.MeshBody,
        plane :adsk.fusion.ConstructionPlane):

        app :adsk.core.Application = adsk.core.Application.get()
        ui :adsk.core.UserInterface = app.userInterface
        sels :adsk.core.Selections = ui.activeSelections

        sels.clear()
        app.executeTextCommand(u'Commands.Start MeshPlaneCutCommand')

        app.executeTextCommand(u'UI.EnableCommandInput MeshPlaneCutBodySel')
        sels.add(mesh)

        app.executeTextCommand(u'UI.EnableCommandInput MeshPlaneCutPlaneSel')
        sels.add(plane)

        app.executeTextCommand(u'Commands.SetString infoCutType MeshPlaneCutSplitBody')
        app.executeTextCommand(u'Commands.SetString infoFillType MeshPlaneCutFillNone')

        app.executeTextCommand(u'NuCommands.CommitCmd')


    def _list_difference(self, list1, list2):
        result = list1.copy()
        for value in list2:
            if value in result:
                result.remove(value)

        return result


    def _getSplitPlane(
        self,
        mesh :adsk.fusion.MeshBody) -> adsk.fusion.ConstructionPlane:

        comp :adsk.fusion.Component = mesh.parentComponent

        pnts = [p for p in mesh.displayMesh.nodeCoordinates]
        bound = adsk.core.BoundingBox3D.create(pnts[0], pnts[1])
        for p in pnts[2:]:
            bound.expand(p)

        pntMin :adsk.core.Point3D = bound.minPoint
        pntMax :adsk.core.Point3D = bound.maxPoint

        pntMid = adsk.core.Point3D.create(
            (pntMin.x + pntMax.x) * 0.5,
            (pntMin.y + pntMax.y) * 0.5,
            (pntMin.z + pntMax.z) * 0.5)

        width = [abs(pntMax.x) + abs(pntMin.x), comp.xConstructionAxis]
        length = [abs(pntMax.y) + abs(pntMin.y), comp.yConstructionAxis]
        height = [abs(pntMax.z) + abs(pntMin.z), comp.zConstructionAxis]

        direction = max([width, length, height], key=(lambda x: x[0]))

        plane = adsk.core.Plane.create(pntMid, direction[1].geometry.direction)
        conPlanes :adsk.fusion.ConstructionPlanes = comp.constructionPlanes
        planeIpt :adsk.fusion.ConstructionPlaneInput = conPlanes.createInput()
        planeIpt.setByPlane(plane)

        return conPlanes.add(planeIpt)

上記で50000枚以下と記載しましたが、10000枚を超えるとこの様な
警告のダイアログが出現します。
f:id:kandennti:20210427171109p:plain
どうせなら放置したい方なのでダイアログを閉じたいのですが、
方法が見つかりませんでした。
その為、10000枚以下になるまでぶった切りまくります。

後はIgesなりStepなりでエクスポートして下さいませ。

ドロップダウンリストを変更する

APIで提供されていない処理をスクリプト/アドインで行う為の唯一の方法が
テキストコマンドを利用する方法です。
APIフォーラムでは、テキストコマンドを利用する方法については、
Autodesk社員&元社員には "沈黙" 又は "否定的" ですが、僕同様のユーザーには
ある程度浸透してくれたようで、他の方がテキストコマンドを利用した方法で
解決されているトピも幾つか見られます。


個人的には、取り組み始めた当初から長い間方法が不明で、非常に不便に
感じていたものが有ります。それは "ドロップダウンリスト" の値の
切り替えです。 ですが、ひょんな事から操作する方法が分かりました。


文字でグズグズ書いても分かりにくいため、もう少し具体的な説明をしましょう。
f:id:kandennti:20210426153047p:plain
ブロックのボディにかかるように円のスケッチが描かれています。
この状態で押し出しコマンドを呼び出し、距離を設定すると "操作" 部分が
"切り取り" の状態になります。この "操作" 部分がドロップダウンリストに
なっており、例えば新規ボディで実行したくても切り替える方法を見つけることが
出来ませんでした。
悪いことに、このドロップダウンリストは多くのコマンドに実装されている為、
テキストコマンドでのコマンドの実行の利用価値を大きく下げている原因と
なっていました。


では、実際にやってみましょう。
以前、こちらを取り組んだパイプコマンドを修正し、以前出来なかった処理を
行ってみます。
テキストコマンド1 - C#ATIA


〇Toolkit.cmdDialog
まず、コマンドのダイアログに表示されるImputsの "target label" を見つける
必要があります。これは以前も記載した内容ですが、一連の流れとして記載しておきます。

ダイアログが表示された状態で、テキストコマンドのこちらを実行します。

Toolkit.cmdDialog 

f:id:kandennti:20210426153310p:plain
この様に、ダイアログ上の情報の取得が出来ます。パイプコマンドの場合は
この様な結果です。

Toolkit.cmdDialog 
====>  CommandDialog:PrimitivePipePanel-4
Path, eSelectionButton, Label=パス, 選択
SWEEP_CHAINING_OPTION, eCheckBox, チェーン選択
infoSectionType, eDropDownButton, Label=断面, 円形
  MenuItems:
  infoCircular, eDropDownItem, 円形
  infoRectangular, eDropDownItem, 正方形
  infoTriangular, eDropDownItem, 三角
PIPE_HOLLOW_OPTION, eCheckBox, 空洞
infoBooleanType, eDropDownButton, Label=操作, 新規ボディ
  MenuItems:
  infoJoinType, eDropDownItem, 結合
  infoCutType, eDropDownItem, 切り取り
  infoIntersectType, eDropDownItem, 交差
  infoNewBodyType, eDropDownItem, 新規ボディ
  infoNewBodyInNewCompType, eDropDownItem, 新規コンポーネント

"Toolkit.cmdDialog" は非表示となっている要素も出力されますが、標示されてる
ダイアログと比べると何となく感じ取る事は出来るはずです。

"====>" が存在している行以降で、先頭にスペースの無い行を見てみると
おおよそこのようなフォーマットになっています。

"target label" , タイプ , ラベル , 値

タイプが "eDropDownButton" となっているものが、ドロップダウンリストとなっており
"MenuItems:" 以降の行が、選択肢となるリストで、この様なフォーマットなっています。

"target label" , タイプ , ラベル

ラベルから、"target label" はこの様になっていますね。
f:id:kandennti:20210426153341p:plain


〇Commands.SetString
今回のお話のメインディッシュはこれです。これこそがドロップダウンリストの値を
変更する為のコマンドです。使い方はこんな感じです。

Commands.SetString <target label> <listItemのtarget label>

例えば "断面" を正方形としたいのであれば、
f:id:kandennti:20210426153431p:plain

Commands.SetString infoSectionType infoRectangular

です。

最初の "操作" 部分が "切り取り" になってしまう事を避ける為、
必ず、 "新規ボディ" としたいのであれば

Commands.SetString infoBooleanType infoNewBodyType

です。


〇よりコントロールされたパイプコマンド
以前のスクリプトを数値を少し変更し、断面を正方形と三角形になる様にし、
それぞれ新規ボディで作成されるように修正しました。

#Fusion360API python script
#Author-kantoku
#Description-create PipeFeature sample

import adsk.core, adsk.fusion, traceback

_app = adsk.core.Application.cast(None)
_ui = adsk.core.UserInterface.cast(None)

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

        # new doc
        _app.documents.add(adsk.core.DocumentTypes.FusionDesignDocumentType)
        des  :adsk.fusion.Design = _app.activeProduct
        des.designType = adsk.fusion.DesignTypes.ParametricDesignType
        root :adsk.fusion.Component = des.rootComponent

        # create sketch
        crv = initSktCircle(root)

        # create pipe
        initPipe(crv, 'infoRectangular', 'infoNewBodyType')

        # create sketch
        crv = initSktSpline(root)

        # create pipe
        initPipe(crv, 'infoTriangular', 'infoNewBodyType')

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

def initSktSpline(comp :adsk.fusion.Component):
    skt :adsk.fusion.Sketch = comp.sketches.add(comp.xYConstructionPlane)

    poss = [[-1,2,0], [2,1,0], [-3,-4,0]]

    pnt3D = adsk.core.Point3D
    objs = adsk.core.ObjectCollection.create()
    [objs.add(pnt3D.create(x,y,z)) for (x,y,z) in poss]
        
    crvs :adsk.fusion.SketchCurves = skt.sketchCurves
    crv = crvs.sketchFittedSplines.add(objs)

    return crv

def initSktCircle(comp :adsk.fusion.Component):
    skt :adsk.fusion.Sketch = comp.sketches.add(comp.xYConstructionPlane)

    pnt3D = adsk.core.Point3D
    crvs :adsk.fusion.SketchCurves = skt.sketchCurves
    crv = crvs.sketchCircles.addByCenterRadius(pnt3D.create(-5.0,-5,0), 4.0)

    return crv

def initPipe(
    path,
    infoSectionType = 'infoCircular',
    infoBooleanType = 'infoNewBodyType'):

    sels :adsk.core.Selections = _ui.activeSelections
    sels.clear()
    sels.add(path)

    txtCmds = [
        u'Commands.Start PrimitivePipe', # show dialog
        u'Commands.SetDouble SWEEP_POP_ALONG 1.0', # input distance
        u'Commands.SetDouble SectionRadius 0.5', # input radius
        u'Commands.SetString infoSectionType {}'.format(infoSectionType),
        u'Commands.SetString infoBooleanType {}'.format(infoBooleanType),
        u'NuCommands.CommitCmd' # execute command
    ]
    
    for cmd in txtCmds:
        _app.executeTextCommand(cmd)

    sels.clear()

結果はこの様になります。
f:id:kandennti:20210426153534p:plain


長い間の悩みが解消されたようです。これが利用出来るようになれば以前以上に
APIからのテキストコマンドの呼び出しが有効な手段となるはずです。