C#ATIA

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

リンクされました

今更ですが、LIC(LiCLOG)さんのブログにリンクされました。
仕様ツリーの構成をExcelファイルに出力するマクロ|CATIAマクロの作成方法 | LiCLOG

気が付くのが遅くて申し訳ないです。
仲良くしてやってください。


当時、"CADのマクロなんだから、CADっぽいサイトにUpした方が
正しい!!" と思い、GrabCADにUpしましたが、今思うとgithub
方が正しいですね・・・。

過去に、客先支給の履歴付きデータを修正し送り返す際、
”ここの辺りを修正しましたよ” と、あちらのマクロを使用した
Excelファイルも一緒に送ったことが何度かあったのですが、
”おぉこれは分かりやすくて助かったよ” と言われたことは
一度もありません。

同一形状のボディを判断する3

こちらの続きです。

前回のスクリプトで異なる形状と判断出来ないものは
対象形状です。
f:id:kandennti:20210623213256p:plain

直方体の上にL字のエンボスを付けた形状の対象形状は、
色々と回転させても一致する事はありませんね。
しかし、前回のアイデアの重心のズレた距離だけでは
判断出来ません。

そこで距離だけでは無く、重心のズレをベクトルとして
受け止め、ベクトル自体が一致するかどうかも条件に
付け加える事で、対象形状を判断することが出来ないかな?
と考えました。

但し、比較対象となるボディが様々な方向になっている
可能性も考慮したいため、慣性モーメントの軸
(PhysicalProperties.getPrincipalAxesメソッド)を利用し
マトリックス(4x4)を作り出し変換した後のベクトルで
比較する事にします。

不安要素は、慣性モーメントの各軸が同一のボディの場合
同じになるものか? (一番長手方向がXAxisになる等)が
良くわかってません。・・・試すしかない。

# Fusion360API Python script
import adsk.core, adsk.fusion, traceback
import dataclasses

_tolerance = 0.0001

@dataclasses.dataclass
class bRepContainer:
    body : adsk.fusion.BRepBody
    faceCount : int
    vartexCount : int
    area : float
    volume : float
    cogLength : float
    cogVecter : adsk.core.Vector3D


    def isMatch(
        self :dataclasses.dataclass,
        cont :dataclasses.dataclass) -> bool:

        if self.faceCount != cont.faceCount:
            return False

        if self.vartexCount != cont.vartexCount:
            return False

        if abs(self.area - cont.area) > _tolerance:
            return False

        if abs(self.volume - cont.volume) > _tolerance:
            return False

        if abs(self.cogLength - cont.cogLength) > _tolerance:
            return False

        if not self.cogLength:
            return True

        # if not self.cogVecter.isEqualTo(cont.cogVecter):
        #     return False

        if not self.cogVecter.isParallelTo(cont.cogVecter):
            return False

        return True

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

        adsk.core.Point3D.dump = dumpPoint3D

        bodies = getAllBRepBodies(des)
        dumpMsg(f'** All Body Count : {len(bodies)} **')

        groups = groupByPhysicaProp(bodies)

        for group in groups:
            dumpMsg(f'-- Area:{group[0].area} , Volume:{group[0].volume} --')
            dumpMsg(f'-- Misalignment of the center of gravity:{group[0].cogLength} --')
            dumpMsg(f'-- FaceCount:{group[0].faceCount} , VartexCount:{group[0].vartexCount} --')
            group[0].cogVecter.asPoint().dump('-- cogVecter:')
            for cont in group:
                dumpMsg(cont.body.name)
                pass
            dumpMsg('')
        a=1

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

def groupByPhysicaProp(
    bodies :list):

    groups = [[bodies[0]]]
    for cont in bodies[1:]:
        findFG = False
        for group in groups:
            if cont.isMatch(group[0]):
                group.append(cont)
                findFG = True

        if findFG:
            continue

        groups.append([cont])

    return groups

def initContainer(
    self :adsk.fusion.BRepBody) -> dataclasses.dataclass:

    # prop
    VERYHI = adsk.fusion.CalculationAccuracy.VeryHighCalculationAccuracy
    prop :adsk.fusion.PhysicalProperties = self.getPhysicalProperties(VERYHI)

    # Misalignment of the center of gravity
    bodyCenter :adsk.core.Point3D = prop.centerOfMass

    measMgr :adsk.core.MeasureManager = adsk.core.Application.get().measureManager
    _, xAxis, yAxis, zAxis = prop.getPrincipalAxes()

    bound :adsk.core.OrientedBoundingBox3D = measMgr.getOrientedBoundingBox(
        self, xAxis, yAxis)
    boundCenter :adsk.core.Point3D = bound.centerPoint
    length :float = bodyCenter.distanceTo(boundCenter)

    vec :adsk.core.Vector3D = adsk.core.Vector3D.cast(None)
    if length > 0:

        mat :adsk.core.Matrix3D = adsk.core.Matrix3D.create()
        mat.setWithCoordinateSystem(boundCenter, xAxis, yAxis, zAxis)
        mat.invert()

        vec = boundCenter.vectorTo(bodyCenter)
        vec.transformBy(mat)

    return bRepContainer(
        self,
        self.faces.count,
        self.vertices.count,
        prop.area,
        prop.volume,
        length,
        vec)

def getAllBRepBodies(
    des :adsk.fusion.Design) -> list:

    adsk.fusion.BRepBody.initContainer = initContainer

    root :adsk.fusion.Component = des.rootComponent
    bodies = [body.initContainer() for body in root.bRepBodies]
    for occ in root.allOccurrences:
        for comp in occ.Component:
            bodies.extend([body.initContainer() for body in comp.bRepBodies])

    return bodies

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

def dumpPoint3D(self :adsk.core.Point3D, haeder = ''):
    dumpMsg(f'{haeder} - x:{self.x} y:{self.y} z:{self.z}')

まぁ、脳みその中で思い付いたことを言葉で表現するのは難しいです。
ちょっとベクトルの判定を "一致" とすべきか "平行" と判定すべきか
迷ってます。


最初の画像のボディで試します。
右がボディ1で左がボディ2です。
f:id:kandennti:20210623214925p:plain
結果はこんな感じです。

** All Body Count : 2 **
-- Area:108.0 , Volume:64.0 --
-- Misalignment of the center of gravity:0.18772889041565421 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:0.1862544021475668 y:0.022587374504154684 z:-0.006422187349668939
ボディ1

-- Area:108.0 , Volume:64.0 --
-- Misalignment of the center of gravity:0.18772889041565421 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:-0.1862544021475668 y:0.022587374504154684 z:-0.006422187349668939
ボディ2

ベクトルの違いから "別の物" と判断してくれました。


安心してはいられないと思い、この様な状態にしました。
f:id:kandennti:20210623215332p:plain
ボディ2を円形状パターンで複製(ボディ3、4)しました。
(=ボディ2,3,4は同じものと判断されたい)

実行結果はこちら。

** All Body Count : 4 **
-- Area:108.0 , Volume:64.0 --
-- Misalignment of the center of gravity:0.18772889041565421 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:0.1862544021475668 y:0.022587374504154684 z:-0.006422187349668939
ボディ1

-- Area:108.0 , Volume:64.0 --
-- Misalignment of the center of gravity:0.18772889041565421 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:-0.1862544021475668 y:0.022587374504154684 z:-0.006422187349668939
ボディ2

-- Area:108.00000000000003 , Volume:64.00000000000004 --
-- Misalignment of the center of gravity:0.18772889133855403 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:0.022587374420952516 y:0.18625440295632148 z:-0.006422191164590873
ボディ3

-- Area:108.0 , Volume:64.00000000000003 --
-- Misalignment of the center of gravity:0.18772888981099467 --
-- FaceCount:10 , VartexCount:16 --
-- cogVecter: - x:0.1862544016243073 y:-0.022587374504283015 z:-0.006422184849648054
ボディ4

・・・全部異なるものと判断されました。

色々調べた結果、原因は最初に懸念していた通り、慣性モーメントで
得られる軸がボディに対して一定のルールでXAxis・・を割り当てられる
わけでは無いようです。(恐らく近い絶対座標の軸になるのかな?)


重心のズレをベクトルとするのは良いアイデアだと思ったのですが、
上手く行きません。他に何か良いアイデアが無いかな?
そもそも確立されたアルゴリズムのようなものがあるのかな?

ボディのクローンを大量に作る

テスト用のデータをその都度チマチマ作るのが辛くなり、
スクリプトで作る事にしました。

# Fusion360API Python script
import adsk.core, adsk.fusion, traceback
import random

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

        msg :str = 'Select'
        selFiltter :str = 'Bodies'
        sel :adsk.core.Selection = selectEnt(msg ,selFiltter)
        if not sel: return

        body :adsk.fusion.BRepBody = sel.entity

        # ここ変える
        cloneRandom(body, 100)

        app.activeViewport.fit()

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

def cloneRandom(
    body :adsk.fusion.BRepBody,
    count : int = 10,
    minPos = -1000,
    maxPos = 1000,):


    def getRandomMatrix3D(
        ) -> adsk.core.Matrix3D:

        Vec3D = adsk.core.Vector3D
        ary = [random.randint(minPos, maxPos) for _ in range(3)]
        vecDmy :adsk.core.Vector3D = Vec3D.create(
            ary[0]*0.1, ary[1]*0.1, ary[2]*0.1)

        ary = [random.randint(minPos, maxPos) for _ in range(3)]
        vecX :adsk.core.Vector3D = Vec3D.create(
            ary[0]*0.1, ary[1]*0.1, ary[2]*0.1)
        vecX.normalize()

        vecY :adsk.core.Vector3D = vecX.crossProduct(vecDmy)
        vecY.normalize()

        vecZ :adsk.core.Vector3D = vecX.crossProduct(vecY)
        vecZ.normalize()

        ary = [random.randint(minPos, maxPos) for _ in range(3)]
        pnt :adsk.core.Point3D = adsk.core.Point3D.create(
            ary[0]*0.1, ary[1]*0.1, ary[2]*0.1)

        mat :adsk.core.Matrix3D = adsk.core.Matrix3D.create()
        mat.setWithCoordinateSystem(pnt, vecX, vecY, vecZ)

        return mat

    # ********
    mats = [getRandomMatrix3D() for _ in range(count)]

    tmpMgr :adsk.fusion.TemporaryBRepManager = adsk.fusion.TemporaryBRepManager.get()
    clones = []
    for mat in mats:
        pass
        clone = tmpMgr.copy(body)
        tmpMgr.transform(clone, mat)
        clones.append(clone)

    comp :adsk.fusion.Component = body.parentComponent
    bodies :adsk.fusion.BRepBodies = comp.bRepBodies
    baseFeats = comp.features.baseFeatures
    baseFeat = baseFeats.add()
    baseFeat.startEdit()
    for clone in clones:
        bodies.add(clone,baseFeat)
    baseFeat.finishEdit()

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

    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        sel = ui.selectEntity(msg, filtterStr)
        return sel
    except:
        return None

スクリプト実行後、ボディを選択すると
f:id:kandennti:20210621124200p:plain
ドーンと100個、ランダムな位置・向きでコピペします。

何も考えていないので、こんな感じで干渉するものも
出来ます。(テストで使用する分には問題無いです)
f:id:kandennti:20210621124412p:plain

同一形状のボディを判断する2

こちらの続きです。
同一形状のボディを判断する1 - C#ATIA
慣性モーメントを利用しての判断を追加することにします。


慣性モーメント自体については・・・え~正しい意味合いは、
検索してみて下さい。
自分の中ではこのようなイメージで捉えています。
f:id:kandennti:20210619145353p:plain
歪な形状では無くても良いのですが、ボディを包囲するような
最小の直方体を作る際のXYZのベクトルや中心が得られる
と思っています。

常に(恐らくですが) "最小" の直方体となるのであれば、同一の
ボディが移動されたり回転されたりしていても、
”同じ状態でボディを見る為の座標系を得られる”
と思ってます。 ・・・多分。

赤がボディの重心で、青が最小の直方体の重心です。
f:id:kandennti:20210619151350p:plain
これを利用すれば、お互いの重心のズレが発生するので
前回の円柱の位置の違いを判断することが出来るのでは
ないのかな? と感じています。
(球体と正多面体は重心の位置ずれ無し)

念の為、過去のAPIフォーラム質疑や自信でテストした事を記載して
おきます。
ボディ(BRepBody)のドキュメントがこちらですが
Fusion 360 Help

慣性モーメントを得ることが出来る、physicalPropertiesプロパティが
ありますが、精度が悪くて使い物になりません。
getPhysicalPropertiesメソッドを利用する事にします。

又、boundingBoxプロパティはルートコンポーネントの原点の
向きでのboundingBox(ボディを包囲する直方体)しか得られない上、
非常に精度が悪い為、MeasureManagerオブジェクトのgetOrientedBoundingBox
メソッドを利用します。
Fusion 360 Help
こちらは向きも自由ですし、精度が比較にならない程良いです。

これらを考慮した上で、こちらを作成しました。

# Fusion360API Python script
import adsk.core, adsk.fusion, traceback
import dataclasses

_tolerance = 0.0001

@dataclasses.dataclass
class bRepContainer:
    body : adsk.fusion.BRepBody
    area : float
    volume : float
    cogMisalignment : float
    faceCount : int
    vartexCount : int

    def isMatch(
        self :dataclasses.dataclass,
        cont :dataclasses.dataclass) -> bool:

        if abs(self.area - cont.area) > _tolerance:
            return False

        if abs(self.volume - cont.volume) > _tolerance:
            return False

        if abs(self.cogMisalignment - cont.cogMisalignment) > _tolerance:
            return False

        if self.faceCount != cont.faceCount:
            return False

        if self.vartexCount != cont.vartexCount:
            return False

        return True

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

        bodies = getAllBRepBodies(des)
        dumpMsg(f'** All Body Count : {len(bodies)} **')

        groups = groupByPhysicaProp(bodies)

        for group in groups:
            dumpMsg(f'-- Area:{group[0].area} , Volume:{group[0].volume} --')
            dumpMsg(f'-- Misalignment of the center of gravity:{group[0].cogMisalignment} --')
            dumpMsg(f'-- FaceCount:{group[0].faceCount} , VartexCount:{group[0].vartexCount} --')
            for cont in group:
                dumpMsg(cont.body.name)
            dumpMsg('')
        a=1

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

def groupByPhysicaProp(
    bodies :list):

    groups = [[bodies[0]]]
    for cont in bodies[1:]:
        findFG = False
        for group in groups:
            if cont.isMatch(group[0]):
                group.append(cont)
                findFG = True

        if findFG:
            continue

        groups.append([cont])

    return groups

def initContainer(
    self :adsk.fusion.BRepBody) -> dataclasses.dataclass:

    # prop
    VERYHI = adsk.fusion.CalculationAccuracy.VeryHighCalculationAccuracy
    prop :adsk.fusion.PhysicalProperties = self.getPhysicalProperties(VERYHI)

    # Misalignment of the center of gravity
    bodyCenter :adsk.core.Point3D = prop.centerOfMass

    measMgr :adsk.core.MeasureManager = adsk.core.Application.get().measureManager
    _, xAxis, yAxis, _ = prop.getPrincipalAxes()
    bound :adsk.core.OrientedBoundingBox3D = measMgr.getOrientedBoundingBox(
        self, xAxis, yAxis)
    boundCenter :adsk.core.Point3D = bound.centerPoint
    length :float = bodyCenter.distanceTo(boundCenter)

    return bRepContainer(
        self,
        prop.area,
        prop.volume,
        length,
        self.faces.count,
        self.vertices.count)

def getAllBRepBodies(
    des :adsk.fusion.Design) -> list:

    adsk.fusion.BRepBody.initContainer = initContainer

    root :adsk.fusion.Component = des.rootComponent
    bodies = [body.initContainer() for body in root.bRepBodies]
    for occ in root.allOccurrences:
        for comp in occ.Component:
            bodies.extend([body.initContainer() for body in comp.bRepBodies])

    return bodies

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

ついでに、面の数と頂点数の一致も一致判断の条件として追加しました。
見直すと色々と不満があるのですけどね。


では、この様なデータでテストします。
f:id:kandennti:20210619152752p:plain
ボディ1とボディ2は円柱の位置違いで、ボディ3はボディ2の円形状パターンで
作成しているので、一致していると判断されたいところです。

結果はこちら。
f:id:kandennti:20210619153108p:plain
”Area” ”Volume” に違いがありますが、非常に小さいため誤差の範囲。
”Misalignment of the center of gravity” が最初に説明した重心の
位置ずれ距離です。

無事、判断出来ています。でも、これで判断出来ないものがある事に、
途中で気が付いちゃいました。

同一形状のボディを判断する1

フォーラムにあった質問で、思い付きで作ったスクリプトがこちら。
Re: モデルのカウントについて - Autodesk Community

あちらのデータは、何個かボディを作りパターンを何度が利用しました。
要は "最初に作った何個か" = "何種類か" を見つけ出したい
と言うお話です。


ボディの位置や向きが異なっても、同じ数値を返してくれそうなのは
表面積と体積かな? と思います。重心位置は駄目ですね。

表面積と体積で一致を判断するスクリプトを作り、都合よく上手く行く
データを用意したので、上手く行きますよね?


問題になりそうなものは、こんな感じです。
f:id:kandennti:20210617174548p:plain
同一のブロックに同一の円柱をくっ付けているのですが、付けている
位置が異なります。 当然、形状的には別の物と判断されるべきです。

ですが、表面積と体積が一致するのであのスクリプトでは判断出来ません。
面の数も頂点数も同じ。で、どうする?

慣性モーメントを取得して、こちらを使ってバウンダリボックス作って
Fusion 360 Help
バウンダリボックスの重心とボディの重心の距離を測ったり・・・
後は何かあるかな? 思い付かない。


”このスクリプトはAI搭載です” って嘘ついて、誤った結果の場合は
"機械学習不足ですね。もっと多くのサンプルを利用して学習させないと
精度が上がりません" ぐらいの言い訳を事前に用意しておくのはどうだろうか?


・・・テーマとしては面白いんだけどなぁ。

エンジン太郎さん

なん重さんのコメント欄に名前が記載されていたので
探したところ・・・態々書くまでも無く凄すぎる。
エンジン太郎 - YouTube

V型2気筒はシリンダーブロック、ヘッド等は市販品っぽいけど、
4気筒はコンロッドまで自作・・・。

治具はかなり勉強になります。

同一平面上の接続した平らな面を色分けする

先日、こちらのお題に取り組みました。
Detect Coplanar Faces - Autodesk Community
テーマとしては面白そうだったので。
f:id:kandennti:20210611185246p:plain
パッと見た感じ、アイアンマンかな?違う?


元が何のデータか不明ですが、ポリゴンデータをFusion360にインポート
したところ、四角ポリゴンが三角ポリゴンに勝手に変換されたようです。
まぁメッシュ作業スペースは三角ポリゴンな世界なのでしょうがないでしょう。

メッシュボディから変換したソリッド面を・・・要は元のポリゴン時と
同じように多角形の面にしたいので色分けしたい、出来るか?
と解釈しました。

最初は1枚1枚法線ベクトルを取得して、隣の面と比較して・・・と、
面倒な事を考えていたのですが、驚くほど簡単なtangentiallyConnectedFaces
プロパティを発見しました。
Fusion 360 Help
接線接続している隣の面を取得するだけでは無く、その先も見てくれてます。
(接線接続が途切れるまで探し出してくれます。探すわけじゃないかも)

groupByFlattenSurfaces関数が、元で1つだった面毎にグループ分けしている
キモとなる関数です。(デバッグ用の無駄コードを消し忘れると言う失態)
実はこれ自体は直ぐ出来ました。

手間取ったのは色を付ける部分。何でFusion360の色はメンドクサイ仕様なんだろうか?
GUIでも面倒)