C#ATIA

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

再発明の失敗

あまり細かな事は書きませんが、こちらに僕がupしたgithub
リンクがあります。
Fusion 360 Internals: Trying to re-invent Fusion’s GUI (and failing)

昨年、記載された御本人から "あなたも興味があるだろう" と
直接メールを頂いて教わりました。
知識としては覚えています(理解はしていない)が、あまり
興味はありませんでした・・・。

アクティブなコンフィギュレーション名

昨年の後半だったかな? Fusion360コンフィギュレーションと言う機能が追加されました。
デモの状態の時から知っていて "おぉすごい" と思っていたのですが、
使い方を知らず・・・と言いますか、試してもいませんでした。

ざっくりですが、どんな機能か?と言いますと、一つのファイルに複数の
類似したデザインを持たす事が出来る機能です。

詳しくはこちらです。(丸投げ)
Help

それなりにやると、ここを切り替えるだけで形状が切り替わります。

凄いですよね。


最近になり、APIにもコンフィギュレーションの機能が追加されました。
そこで、こちらの質問を見つけました。
Name of the current configuration? - Autodesk Community
アクティブなコンフィギュレーションの名前はどうやって取得するのか?
と言う事の様です。確かにHelpを見ても取得の仕方がわからないです。
(そもそも使ったことの無い機能なので、何もかもわからないです)

で、あちらにレスしましたが、探し回った結果こんな感じでした。

# Fusion360API Python script

import traceback
import adsk.core as core
import adsk.fusion as fusion

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

        if not des.isConfiguredDesign: return

        configTopTable: fusion.ConfigurationTopTable = des.configurationTopTable
        actRow: fusion.ConfigurationRow = configTopTable.activeRow
        ui.messageBox(actRow.name, "Active Configuration Name")

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

何となく、Designオブジェクトにありそうだな・・・とは目星を付けていました。
configurationTopTableプロパティからゴリゴリと って感じですね。

イナーシャの取得

イナーシャが欲しいのですが、ササっと検索してもProductばかり見つかったのですが
だったのですが、サーフェスで欲しいです。

結局、全部入りのサイトがありました。
Measuring Mass and Inertia | CATIA V5 Automation

消えてしまうと困るので、お借りする。
まず単位はこちら。

メートル法っぽいです。

で、
Product(Partも) : GetTechnologicalObject
Body(形状セットも) : SPAWorkbench.Inertias.Add
で、サーフェスは直接の方法が無い様なのですが、新規の形状セットに
突っ込んで、形状セットのイナーシャで大丈夫だよ と
記載されている気がします。

そこで、好みな部分も有ってこんな感じにしてみました。

'イナーシャの取得
'''param:surf-対象サーフェス
'''return:array(Inertia, HybridBody) - 失敗:empty
private Function get_surf_inertia( _
    Byval surf As HybridShape) As variant

    set get_surf_inertia = empty

    If surf Is Nothing Then exit function

    dim pt As Part
    set pt = get_parent_of_T(surf, "Part")

    Dim objRef As Reference
    Set objRef = pt.CreateReferenceFromObject(surf)

    Dim intType As Integer
    intType = pt.HybridShapeFactory.GetGeometricalFeatureType(objRef)

    If intType = 5 Then exit function

    Dim geoSet As HybridBody
    Set geoSet = pt.HybridBodies.Add
    geoSet.Name = "Temp_ForInertiaMeasure"

    Dim objJoin As HybridShapeAssemble
    Set objJoin = pt.HybridShapeFactory.AddNewJoin(objRef, objRef)
    objJoin.RemoveElement 2
    pt.UpdateObject objJoin

    geoSet.AppendHybridShape objJoin

    On Error Resume Next

    Dim objSPAWorkbench As Workbench
    Set objSPAWorkbench = pt.Parent.GetWorkbench("SPAWorkbench")

    Dim objInertia As Inertia
    Set objInertia = objSPAWorkbench.Inertias.Add(geoSet)

    On Error GoTo 0

    if objInertia is nothing then exit function

    get_surf_inertia = array(objInertia, geoSet)

End Function


'指定した型をParentから取得 - typenameで取得出来るもので
'''param:aoj-対象要素
'''param:T-目的の型名
'''return:AnyObject - 失敗:Nothing
private Function get_parent_of_T( _
    ByVal aoj As AnyObject, _
    ByVal T As String) _
    As AnyObject

    Dim aojName As String
    Dim parentName As String
    
    On Error Resume Next
        Set aoj = as_dispath(aoj)
        aojName = aoj.name
        parentName = aoj.Parent.name
    On Error GoTo 0

    If TypeName(aoj) = TypeName(aoj.Parent) And _
            aojName = parentName Then
        Set get_parent_of_T = Nothing
        Exit Function
    End If
    If TypeName(aoj) = T Then
        Set get_parent_of_T = aoj
    Else
        Set get_parent_of_T = get_parent_of_T(aoj.Parent, T)
    End If

End Function


'ディスパッチ
'''param:o-対象要素
'''param:T-目的の型名
'''return:CATBaseDispatch
Private Function as_dispath( _
    byval o As INFITF.CATBaseDispatch) As INFITF.CATBaseDispatch

    Set as_dispath = o

End Function

関数だけなので動かないですし、VSCodeで書いただけで動かしていないので
間違っている可能性が大。

BrowserCommandInputへ初期値の受け渡し

久々にFusion360です。
こちらを答えてみました。
How to create a `BrowserCommandInput` and populate it with initial data from Fusion? - Autodesk Community

"ダイアログを表示する時にBrowserCommandInputに初期値を渡したい”
と言う質問だと受け止めて答えてみました。
BrowserCommandInputはダイアログ上にHTMLを表示させる
コマンドインプットです。
全く使った事が無いのですが、仕組み的には全くパレットと同じです。

コードを見るとCommandCreatedイベント時にやろうとしていますが、
タイミングが違うんですよ。
CommandCreatedイベント時はコマンド自体が出来ておらず、イベント
終了後にコマンドが出来上がります。

その為、初期値の受け渡しの python -> javascript の一連の動きは

pythonでコマンド作成 -> ダイアログ表示 -> javascriptのDOMContentLoadedイベント発火
-> javascript側で"HTML読み込んだよ"と送信 -> pythonのincomingFromHTMLイベント発火 
-> pythonで必要な情報を戻り値として送信 -> javascriptで情報を受け取り -> html側に反映

と結構な処理をさせる必要があります。
それともう一つ。BrowserCommandInputで表示させているブラウザは、javascript
非同期処理で書かなければなりません。


フォーラムではファイルを添付して中身が見れないので、こちらで
公開しておきます。(こちらはスクリプトです)
まずはエントリーポイントとなる、"BrowserCommandInputInitialDataTest.py"

# Fusion360API Python script
import traceback
import adsk
import adsk.core as core
import json

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

CMD_INFO = {
    'id': 'kantoku_BrowserCommandInputInitialDataTest',
    'name': 'test',
    'tooltip': 'test'
}

_browserIpt: core.BrowserCommandInput = None

class MyCommandCreatedHandler(core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: core.CommandCreatedEventArgs):
        try:
            global _handlers
            cmd: core.Command = core.Command.cast(args.command)
            inputs: core.CommandInputs = cmd.commandInputs

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

            onHTMLEvent = MyHTMLEventHandler()
            cmd.incomingFromHTML.add(onHTMLEvent)
            _handlers.append(onHTMLEvent)

            global _browserIpt
            _browserIpt = inputs.addBrowserCommandInput(
                "_browserIptId",
                "Active Document Name:",
                "index.html",
                50,
            )

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


class MyHTMLEventHandler(core.HTMLEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args):
        try:
            htmlArgs = core.HTMLEventArgs.cast(args)

            app: core.Application = core.Application.get()
            if htmlArgs.action == 'htmlLoaded':
                args.returnData = json.dumps({
                    "name": app.activeDocument.name,
                })
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


class MyCommandDestroyHandler(core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: core.CommandEventArgs):
        adsk.terminate()


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

        cmdDef: core.CommandDefinition = _ui.commandDefinitions.itemById(
            CMD_INFO['id']
        )

        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                CMD_INFO['id'],
                CMD_INFO['name'],
                CMD_INFO['tooltip']
            )

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

続いて、表示されるHTMLファイル(index.html)。
(javascriptもこれに書き込んでます)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>
      <label id="activeDocName">Unknown</label>
    </div>
  </body>

  <script>
    document.addEventListener("DOMContentLoaded", () => {
      let adskWaiter = setInterval(() => {
        if (window.adsk) {
          clearInterval(adskWaiter);

          adsk.fusionSendData("htmlLoaded", "").then((ret) => {
            let data = JSON.parse(ret);

            let activeDocNameLabel = document.getElementById("activeDocName");
            activeDocNameLabel.textContent = data["name"];
          }, 100);
        }
      });
    });
  </script>
</html>

VBE.CommandBars

こんなの知らなかった・・・。

'vba
Option Explicit

Sub commandBar_test()
    Dim vbe As Object
    Set vbe = Application.vbe
    
    'CommandBar
    Dim cBar As Object
    Set cBar = get_commandBar("Custom1")

    'CommandControl
    Dim myControl
    For Each myControl In cBar.Controls
        Call myControl.Delete
    Next
    
    Set myControl = cBar.Controls _
        .Add(Type:=msoControlComboBox, before:=1)
    With myControl
        .AddItem Text:="First Item", Index:=1
        .AddItem Text:="Second Item", Index:=2
        .DropDownLines = 3
        .DropDownWidth = 75
        .ListHeaderCount = 0
    End With
    
End Sub


'https://learn.microsoft.com/ja-jp/office/vba/api/office.commandbars
Private Function get_commandBar( _
    ByVal name As String) As Object

    Dim vbe As Object
    Set vbe = Application.vbe
    
    On Error Resume Next
    
    Dim cmdBar As Object
    Set cmdBar = vbe.CommandBars.Item(name)
    
    On Error GoTo 0
    
    If cmdBar Is Nothing Then
        Set cmdBar = vbe.CommandBars.Add( _
            name:=name, _
            Position:=msoBarFloating _
        )
    End If
    cmdBar.Visible = True
    
    Set get_commandBar = cmdBar
    
End Function

実行するとこんなのが表示されます。

久々にVBAで刺激を感じた・・・可能性が高まる。

VBAのリスト問題5

こちらの続きです。
VBAのリスト問題4 - C#ATIA

いよいよ先人の方々が作られたものを試す事にします。
githubで見ても、結構な数が公開されていますね。いかに他の方々も
悩まれているというか、不満に思っているかが分かります。

多すぎて、とても全ては試せませんね。
チラチラっと見た感じですが、中身はCollectionのものと配列のもの
がありました。・・・個人的にはCollectionの利用は考えないけど。

で、とりあえずこちらを試す事としました。
GitHub - nanbu/XArray: Array class for VBA
中は配列で、1クラスにまとまってますし、日本人の方だったので
親近感がありました。

もう一つは、過去に試した事があるariawaseです。
GitHub - vbaidiot/ariawase: Ariawase is free library for VBA cowboys.
・・・9年前かな?
一緒に公開されている "vbac.wsf" ばかりが目につきますが、
ariawaseは本当にすごいです。僕の知る限り、VBAのライブラリ
としては桁違いに高水準のものです。
但し、困るのはHelpらしきものが無いので、理解しきれない。

前回のものも含めてこんな感じでテストです。

vba

Option Explicit

Private sw_ As StopWatch

Sub Custom_Array_test()

    Dim count As Long
    count = 500000
 
    Set sw_ = New StopWatch
    
    'XArray
    sw_.start
    Debug.Print "-- xArray--"
    
    Call test_xarray(count)
    Call sw_.total("Total" & ":")

    'ariawase
    sw_.start
    Debug.Print "-- ariawase --"
    
    Call Ext.ArrRange(0, count)
    Call sw_.total("Total" & ":")
    
    'preserve一度で
    sw_.start
    Debug.Print "-- 一度で--"

    Call preserve_once(count)
    Call sw_.total("Total" & ":")

    sw_.start
    Debug.Print "-- 毎回 --"
    
    Call preserve_every_time(count)
    Call sw_.total("Total" & ":")

End Sub

Private Sub test_xarray( _
    ByVal count As Long)
    
    Dim xArr As Object
    Set xArr = New XArray
    
    Dim i As Long
    For i = 0 To count - 1
        Call xArr.Add(i)
    Next
    
End Sub

Private Sub preserve_once( _
    ByVal count As Long)
    
    Dim ary() As Long
    ReDim ary(count)
    
    Dim i As Long
    For i = 0 To count - 1
        ary(i) = i
    Next
    
End Sub

Private Sub preserve_every_time( _
    ByVal count As Long)
    
    Dim ary() As Long
    
    Dim i As Long
    For i = 0 To count - 1
        ReDim Preserve ary(i)
        ary(i) = i
    Next
    
End Sub

結果はこちら

-- xArray--
Total:7.953125s
-- ariawase --
Total:1.421875s
-- 一度で--
Total:0.0234375s
-- 毎回 --
Total:0.8125s

おっと意外。毎回ReDim Preserveの方が、ariawaseより
速いですね。でも、ariawaseはあの1行だけで済むんですよね。

VBAのリスト問題4

こちらの続きです。
VBAのリスト問題3 - C#ATIA
進みが悪い・・・。

続いて動的配列です。

Long型だけで比べるとこんな感じです。

**可変長配列**
Long 代入:0.0234375s
呼出:0.0234375s

**可変長配列 Preserve**
Long 代入:0.9375s
呼出:0.0234375s

呼び出しは誤差程度なのですが、代入には差があります。
まぁ当然だとは思います。

代入だけをこの様な感じで再度テストします。

'vba

Option Explicit

Private sw_ As StopWatch

Sub ReDim_Preserve_test()

    Dim count As Long
    count = 1000000
 
    '一度で
    Set sw_ = New StopWatch
    sw_.start
    Debug.Print "-- 一度で --"
    
    Call preserve_once(count)
    Call sw_.total("Total" & ":")

    '毎回
    Set sw_ = New StopWatch
    sw_.start
    Debug.Print "-- 毎回 --"
    
    Call preserve_every_time(count)
    Call sw_.total("Total" & ":")
    
End Sub

Private Sub preserve_every_time( _
    ByVal count As Long)
    
    Dim ary() As Long
    
    Dim i As Long
    For i = 0 To count - 1
        ReDim Preserve ary(i)
        ary(i) = i
        If i Mod 100000 = 0 Then
            Call sw_.split(CStr(i) & ":")
        End If
    Next
    
End Sub

Private Sub preserve_once( _
    ByVal count As Long)
    
    Dim ary() As Long
    ReDim ary(count)
    
    Dim i As Long
    For i = 0 To count - 1
        ary(i) = i
        If i Mod 100000 = 0 Then
            Call sw_.split(CStr(i) & ":")
        End If
    Next
    
End Sub

結果を見やすくするとこんな感じです。

最初に必要なサイズを確保した場合、書き出す意味が無いぐらい
速いですね・・・。
そこが目的では無く、毎回ReDim Preserveしている方は、
後半に向けて徐々に遅くなっています。
これは理由を知ってます。

配列の場合、メモリはコレクションの時と異なり連続した状態で確保されます。
その為、配列要素の1個のサイズが確定すれば、100個先だろうが1万個先だろうが
どこに目的の要素があるかが分かっている為、呼び出しも高速です。

ReDim Preserveが徐々に遅くなる原因は、こんな理由でしょう。
サイズが5個から6個になる場合、6個目は5個目の隣のメモリを確保する
訳ではないです。

まず、6個分のメモリを確保します。これがReDim。
続いて元の5個分の内容をコピーする。これがPreserve。

これを1個サイズを大きくする度に行っているので、
後半になればなるほど、コピーする量が多くなり遅くなる
のだと思っています。

・・・とは言え、コレクションより遥かに速い。