C#ATIA

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

3Dな線のエクスポート1

こちらの続きです。
3Dな線のエクスポート考え中 - C#ATIA

コメント欄にも書いたのですが、行列の積は交換法則成り立たないんですね。
行列の積の交換法則 | 高校数学応援ブログ web問題集
スケッチ位置を変換してから、オカレンスの位置を変換にしたら上手くいきました。

#FusionAPI_python ExportWire Ver0.0.1
#Author-kantoku
#Description-表示されている全てのスケッチの線をエクスポート
#コンストラクションは変換しない

import adsk.core, adsk.fusion, traceback
from itertools import chain
import os.path

def run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        doc = app.activeDocument
        des = adsk.fusion.Design.cast(app.activeProduct)
        
        #表示されているスケッチ
        skts = [skt
                for comp in des.allComponents if comp.isSketchFolderLightBulbOn
                for skt in comp.sketches if skt.isVisible]
        ui.activeSelections.clear()
        
        #正しい位置でジオメトリ取得       
        geos = list(chain.from_iterable(GetSketchCurvesGeos(skt) for skt in skts))
        if len(geos) < 1:
            ui.messageBox('エクスポートする線がありません')
            return
        
        #ファイルパス
        path = Get_Filepath(ui)
        if path is None:
            return
        
        #新規デザイン
        expDoc = NewDoc(app)
        expDes = adsk.fusion.Design.cast(app.activeProduct)
        doc.activate()
        
        #ダイレクト
        expDes.designType = adsk.fusion.DesignTypes.DirectDesignType

        #tempBRep
        tmpMgr = adsk.fusion.TemporaryBRepManager.get()
        crvs,_ = tmpMgr.createWireFromCurves(geos)
        
        #実体化
        expRoot = expDes.rootComponent
        bodies = expRoot.bRepBodies
        bodies.add(crvs)
        
        #保存
        res = ExportFile(path,expDes.exportManager)
        
        #一時Docを閉じる
        expDoc.close(False)
        
        #おしまい
        if res:
            msg = 'Done'
        else:
            msg = 'エクスポート出来ませんでした'
        
        ui.messageBox(msg)
        
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

def ExportFile(path,expMgr):
    _, ext = os.path.splitext(path)
    
    if 'igs' in ext:
        expOpt = expMgr.createIGESExportOptions(path)
    elif 'stp' in ext:
        expOpt = expMgr.createSTEPExportOptions(path)
    elif 'sat' in ext:
        expOpt = expMgr.createSATExportOptions(path)
    else:
        return False
        
    expMgr.execute(expOpt)
    return True
    
#ファイルパス
def Get_Filepath(ui):
    dlg = ui.createFileDialog()
    dlg.title = '3DCurvesExport'
    dlg.isMultiSelectEnabled = False
    dlg.filter = 'IGES(*.igs);;STEP(*.stp);;SAT(*.sat)'
    if dlg.showSave() != adsk.core.DialogResults.DialogOK :
        return
    return dlg.filename
        
def NewDoc(app):
    desDoc = adsk.core.DocumentTypes.FusionDesignDocumentType
    return app.documents.add(desDoc)

def GetSketchCurvesGeos(skt):
    if len(skt.sketchCurves) < 1:
        return None
    
    #extension
    adsk.fusion.SketchCurve.toGeoTF = SketchCurveToGeoTransform
    adsk.fusion.Component.toOcc = ComponentToOccurrenc
    
    mat = skt.transform.copy()
    occ = skt.parentComponent.toOcc()
    
    if not occ is None:
        mat.transformBy(occ.transform)
        
    geos = [crv.toGeoTF(mat) for crv in skt.sketchCurves if not crv.isConstruction]
    
    return geos

#adsk.fusion.SketchCurve
def SketchCurveToGeoTransform(self,mat3d):
    geo = self.geometry.copy()
    geo.transformBy(mat3d)
    
    return geo

#adsk.fusion.Component 拡張メソッド
#コンポーネントからオカレンスの取得 ルートはNone
def ComponentToOccurrenc(self):
    root = self.parentDesign.rootComponent
    if self == root:
        return None
        
    occs = [occ
            for occ in root.allOccurrencesByComponent(self)
            if occ.component == self]
    return occs[0]

まだ、ちょっとエラーになる時があり、調査中ですが
基本的に手動でスケッチに描いた線はエクスポート出来る
はずです。

こちらで確認はしています。
Autodesk Viewer | Free Online File Viewer

ShapeManager

先日こちらの記述を見つけました。
.satにもいろいろある : Solidworksで出力した.satファイルがFusion360で開けない理由 - Qiita
もっと細かな理由を知ってます。何時かは書こうと思っていたので、
これを機に・・・・。

Fusion360のSATファイルはバージョン7以下しかインポート
出来ないことはAutodeskのサイトでもアナウンスされています。
Fusion 360でSATファイルをアップロードまたは開くことができません | Fusion 360 | Autodesk Knowledge Network

あちらの方にもリンクがありましたが、wikipediaのACISの下のほうに
バージョンについて記載されています。
ACIS - Wikipedia
バージョン7については表に記載されていないのですが、恐らく2000年頃
じゃないかと思います。幸いSpace-eがACISカーネルで、異なる
バージョンで保存する事が出来そうです。
f:id:kandennti:20181016181927p:plain
バージョン7はかなり古いです。

何故Autodeskの扱うACISはバージョン7なのか?
と言うお話です。

ACISはDassault Systemes傘下のSpatial Corporationが開発した
カーネルですが、この企業は2000年に買収されています。
Dassault Systemes Completes Acquisition of Spatial Component Business
記述を見付ける事が出来なかったので、記憶で記載しますが
その際、Autodeskにも買収する機会があったようなのですが
カーネルビジネスは行わない」と言うスタンスだった様です。
(現在もカーネルのみの販売は行っていないと思います)

この買収後、DassaultとAutodesk間でゴタゴタが始まり
結果的に、Autodeskは独自のカーネルShapeManager(ASM)を
作る事になったのだろうと思われます。
https://en.wikipedia.org/wiki/ShapeManager
ACISのバージョン7までは権利を持っているようです。

個人的には、触る機会があるCADが
・CATIA V5:CGM
Fusion360:ShapeManager
・Space-e:ACIS
で出所が同じな為か、変換は比較的不満無く出来ています。
(以前はParasolidは苦労しましたが、Fusion360は良くやってくれています)

とは言え、「対応フォーマットACIS(SAT)」と書かれていても、さすがに
バージョン7は古すぎるような気がします。
と思っているのですが、先日Blogに書いた "TemporaryBRepManager"
のexportToFileメソッドとcreateFromFileメソッドは、
ACISフォーマットだけ対応しているんですよね。
Fusion 360 Help
Fusion 360 Help
何故、ACIS推しなんだろう?

3Dな線のエクスポート考え中

中々時間の確保が出来ないので、Blogに書けそうなことは何もしていませんが
少し前にこちらをフォーラムに記載しました。

3Dな線を3DCAD中間ファイルでエクスポートする - Autodesk Community

これ自体を探していたのではなく "3D間違い探しを解く" をもっと早く処理
出来ないかなぁ と試していたところ偶然見つけました。

チラッとだけ処理させている "TemporaryBRepManager" と言うものが
あるのですが、これがFusion360内で形状を表示させる第3の方法だと
思っています。

折角見つけたのでこちらも育ててみたいとは思っているのですが、
フォーラムに "実験的なスプリクト" と書いたように、本当に不完全です。

問題点は2個。
①閉じた形状でエラー
②移動させているコンポーネント内の要素が正しい位置にエクスポートされていない

①についてはインポート時も思うように行かなかったので、複数に分割するしか
方法が無いだろうと思っています。(きっとFusion360は、そうなんだろうと受け止めてます)
問題は②の方で、少し試しているのですが上手く行かないです。
…正確に書けば、移動しているコンポーネントでXY平面以外をサポートと
しているスケッチ要素の変換が上手く行っていないです。
恐らく行列の演算を間違えている…。

3D間違い探しを解く2

こちらの続きです。
3D間違い探しを解く1 - C#ATIA

もうちょっと修正したかったのですが、時間の確保が出来なくなりそうな為
公開しておきます。

#FusionAPI_python FindUnmatchedFaces Ver0.0.1
#Author-kantoku
#Description-3D間違い探しを解く

import adsk.core, adsk.fusion, traceback
import time
import bisect as bs

title = 'FindUnmatchedFaces'
    
def run(context):
    ui = None
    try:
        #モロモロ
        app = adsk.core.Application.get()
        ui = app.userInterface
        des = adsk.fusion.Design.cast(app.activeProduct)
        
        #アクティブコンポ控え
        actComp = des.activeComponent
        
        #選択
        sel = Sel('最初のBodyを選択','SolidBodies')
        if sel is None:
            return
        body1 = sel.entity
        cnt1 = len(body1.faces)
            
        sel = Sel('二番目のBodyを選択','SolidBodies')
        if sel is None:
            return
        body2 = sel.entity        
        cnt2 = len(body2.faces)
        
        #同一チェック
        Refresh()
        if body1 == body2:
            ui.messageBox('同じBodyです!!')
            return
        
        #確認
        msg = ['1){} - 面の数 : {}'.format(body1.name,cnt1),
               '2){} - 面の数 : {}'.format(body2.name,cnt2),
               '比較しますか?']
        if ui.messageBox('\n'.join(msg),title,1,1) == 1:
            return
        
        #拡張
        adsk.fusion.Component.toOcc = toOccurrenc
        adsk.fusion.Component.activate = compActivate
        
        t = time.time()
        
        #コンポーネントの位置
        mat1a,mat2a,mat2_1 = GetMatrix(body1,body2)
        
        #異なる面取得
        faces1,faces2 = GetUnmatchedFaces(body1,body2,mat2_1,0.001)
        
        #同じ?
        Refresh()
        hit1 = len(faces1)
        hit2 = len(faces2)
        if hit1<1 and hit2<1:
            ui.messageBox('違いを見つけることが出来ませんでした',title,0,0)
            return
                
        #安全策 10%
        #あまり異なると処理に時間がかかる上、比較自体に意味が無い
        if (hit1*100//cnt1)>10 or (hit2*100//cnt2)>10:
            msg = ['1){} - 異なる面数/全体数 : {}/{}'.format(body1.name,hit1,cnt1),
                   '2){} - 異なる面数/全体数 : {}/{}'.format(body2.name,hit2,cnt2),
                   '異なる部分が多数有ります。処理を続けますか?']        
            if ui.messageBox('\n'.join(msg),title,1,1) == 1:
                return      
        
        #オフセット面作成
        CreateZeroOffset(faces1,body1.name)
        CreateZeroOffset(faces2,body2.name)
        
        #コンポーネントの位置戻し
        mat1b,mat2b,_ = GetMatrix(body1,body2)
        MoveOcc(body1,mat1a,mat1b)
        MoveOcc(body2,mat2a,mat2b)
        
        #終わり
        actComp.activate()
        Refresh()
        
        msg = ['1){} - 異なる面数/全体数 : {}/{}'.format(body1.name,hit1,cnt1),
               '2){} - 異なる面数/全体数 : {}/{}'.format(body2.name,hit2,cnt2),     
               'time : {:.2f}s'.format(time.time()-t)]
               
        ui.messageBox('\n'.join(msg),title,0,0)
        
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

#異なる面の検索
def GetUnmatchedFaces(body1,body2,mat,tolerance=0.001):
    if body1 is None or body2 is None:
        return
    
    adsk.fusion.BRepFace.isOverRap = False
    adsk.fusion.BRepFace.transform_centroid = None
    
    faces1 = sorted(body1.faces, key=lambda v: v.area)
    faces2 = sorted(body2.faces, key=lambda v: v.area)
    areas2 = [face.area for face in faces2]
    
    for face in faces2:
        face.transform_centroid = TransformByClone(face.centroid,mat) 
    
    for f1 in faces1:
        if f1.isOverRap:
            continue
        
        f1cog = f1.centroid
        lwr = bs.bisect_left(areas2,f1.area - tolerance)
        upr = bs.bisect_right(areas2,f1.area + tolerance)
        
        for f2 in faces2[lwr:upr]:
            if f2.isOverRap == True:
                continue
            
            if f1cog.isEqualToByTolerance(f2.transform_centroid, tolerance):
                f1.isOverRap = True
                f2.isOverRap = True
    
    fs1 = [f for f in faces1 if f.isOverRap == False]
    fs2 = [f for f in faces2 if f.isOverRap == False]
    
    return fs1,fs2
    
#オフセット面
def CreateZeroOffset(faces,header):
    if len(faces) < 1:
        return
    
    zero = adsk.core.ValueInput.createByString('0 cm')
    newBody = adsk.fusion.FeatureOperations.NewBodyFeatureOperation
    
    comp = faces[0].body.parentComponent
    offsets = comp.features.offsetFeatures
    
    objs = lst2objs(faces)
    comp.activate()
    
    offsetbodies = offsets.add(
                    offsets.createInput(objs, zero, newBody, False))
    
    for b in offsetbodies.bodies:
        b.name = header + '_UnmatchedFaces' 

#オカレンス移動
def MoveOcc(body,matA,matB):
    if matA == matB:
        return
        
    occ = body.assemblyContext
    if occ is None:
        return
    
    mat = matB.copy()
    mat.invert()
    mat.transformBy(matA)
    
    occ.transform = mat
    
#オカレンス位置
def GetMatrix(body1,body2):
    occ1 = body1.assemblyContext
    mat1 = adsk.core.Matrix3D.create()
    if not occ1 is None:
        mat1 = occ1.transform
        
    occ2 = body2.assemblyContext
    mat2 = adsk.core.Matrix3D.create()
    if not occ2 is None:
        mat2 = occ2.transform
    
    #occ2→occ1へのマトリックス
    mat2_1 = mat2.copy()
    mat2_1.invert()
    mat2_1.transformBy(mat1)
    
    return mat1,mat2,mat2_1    

#選択
def Sel(msg, selFilter):
    app = adsk.core.Application.get()
    ui  = app.userInterface
    try:
        return ui.selectEntity(msg, selFilter)
    except:
        return None
        
#リフレッシュ 効果無さそう・・・
def Refresh():
    app = adsk.core.Application.get()
    ui = app.userInterface
    ui.activeSelections.clear()
    app.activeViewport.refresh()

#Point3D 変換後のクローン作成
def TransformByClone(pnt,mat):
    p = pnt.copy()
    p.transformBy(mat)
    return p

#List to ObjectCollection
def lst2objs(lst):
    objs = adsk.core.ObjectCollection.create()
    [objs.add(obj) for obj in lst]    
    return objs

#-- 拡張メソッド --
#adsk.fusion.Component 拡張メソッド
#コンポーネントのアクティブ化
def compActivate(self):
    des = self.parentDesign
    occ = self.toOcc()
    if occ is None:
        des.activateRootComponent()
    else:
        occ.activate()
        
#adsk.fusion.Component 拡張メソッド
#コンポーネントからオカレンスの取得 ルートはNone
def toOccurrenc(self):
    root = self.parentDesign.rootComponent
    if self == root:
        return None
        
    occs = [occ
            for occ in root.allOccurrencesByComponent(self)
            if occ.component == self]
    return occs[0]

比較条件は単純で、お互いのBodyの全ての面の表面積と重心を取得し
一致しなかった面だけを抽出しているだけです。

CATIAで作った際は、重心位置を元に八分木を利用し組み合わせの
最適化を図りましたが、今回はそんな面倒なことを止め、
表面積でソートし、重心位置がトレランス以内か?をチェックする
だけにしました。恐らくその方が効率が良いと思ったので。
(ミラーボールのような同じ面積のものが大量に存在すると
 ほぼ総当りにはなるのですが・・・)

恐らく同じものであれば、CATIAの時より短時間で処理できると
思っています。
Fusion360の方が各面の重心・表面積をプロパティで所持している為
アクセスが早いように感じています。
Pythonの内包表記や拡張メソッド・プロパティなんかも、効率良く
感じました。

出来れば、もう少しコマンドっぽくしたい所。

カスタムグラフィックを削除する

Fusion360には、画面上に要素(面・線等)を表示させる方法が複数あります。

その内のひとつにカスタムグラフィックと言う機能が有り、
通常のBRepBody等を作成するより、素早く画面上に表示させます。
・・・APIのHelpのエラーが酷く、リンクさせることが出来ません。
f:id:kandennti:20181003154710p:plain
赤い面がそうなのですが、よくわからないと思います。

但し、このカスタムグラフィックで作成した面等は、触る事が
出来ないわけでもないのですが、スケッチのサポートにしたり
トリムのツールにしたりすることが出来ない為、本当に表示させている
だけの状態のものです。

しかも非表示にすることが出来ない上、削除も手動で行う方法が
無さそうな為、まとめて削除するスプリクトを作成しました。

#FusionAPI_python CustomGraphicsCleaner Ver0.0.1
#Author-kantoku
#Description-カスタムグラフィックを削除する

import adsk.core, traceback
from itertools import chain

def run(context):
    title = 'CustomGraphicsCleaner'
    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        des = adsk.fusion.Design.cast(app.activeProduct)
        comps = des.allComponents
        
        customs = list(chain.from_iterable(
            [c.customGraphicsGroups for c in comps]))
        cnt = len(customs)
        
        msg = ''
        if cnt < 1:
            msg = 'このファイルにはカスタムグラフィック要素はありません!!'
            ui.messageBox(msg,title,0,2)
            return
            
        msg = 'このファイルには{}個のカスタムグラフィックがあります'.format(cnt)
        msg += '\n全て削除しますか?'
        
        if ui.messageBox(msg,title,1,1) == 1:
            return
            
        [c.deleteMe() for c in customs]
        
        ui.messageBox('Done')
        
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

カスタムグラフィックは基本的に、作成したアドイン等のプレビュー機能として
利用するものだろうと思います。

3D間違い探しを解く1

少し使い物になりそうな感じになってきたので、予告。

以前CATIA用に作ったこちらを、Fusion360でも出来ないか?
と模索中です。
二つのボディ/形状セットを比較して、差分を抽出する2 - C#ATIA

f:id:kandennti:20181003150521p:plain
こんな感じの二つのBodyがあります。・・・昔、GrabCADから頂きました。
この二つは微妙な違いで、変更前・変更後みたいな事を想定しています。

f:id:kandennti:20181003150532p:plain
スプリクトを実行すると、こんなダイアログが出ます。
お互い3000枚弱の面が存在し、13枚づつ異なる面があると言うことです。
処理時間は20秒弱。苦労しましたここまで時間を短縮させるのに・・・。

f:id:kandennti:20181003150540p:plain
お互い異なる面を抽出します。ここ連日行っていたオフセットマクロは
このためです。

異なる面を探し出すのは、上記のデータでも6~7秒ほど。面の抽出が
結構時間がかかり悩んでいます。思いつく限り、色々な方法で試したのですが
0mmのオフセット面を一気に作成するのが一番早かったです。

もうちょっと機能を付け足したい。

オフセット面の作成場所3

小出しになっちゃいますが、こちらの続きです。
オフセット面の作成場所2 - C#ATIA

ルートコンポーネントのアクティブ化がわからなかったのですが、
Designオブジェクトに、そのままズバリのネーミングな
activateRootComponentメソッドがありました。 ん~こんなところに
有っても気が付かないです。

#FusionAPI_python test_offset3
#Author-kantoku
#Description-オフセット面の作成

import adsk.core, adsk.fusion, traceback
 
def run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui  = app.userInterface
        des = adsk.fusion.Design.cast(app.activeProduct)
        
        actOcc = des.activeOccurrence
        
        #拡張
        adsk.fusion.Component.toOcc = toOccurrenc
        
        sel = Sel('Select Solid Face','SolidFaces')
        if sel is None:
            return
            
        face = sel.entity
        CreateZeroOffset(face)
        
        OccActivate(actOcc)
        
        ui.messageBox('Done')
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

def Sel(msg, selFilter):
    app = adsk.core.Application.get()
    ui  = app.userInterface
    try:
        return ui.selectEntity(msg, selFilter)
    except:
        return None
    
def CreateZeroOffset(face):
    zero = adsk.core.ValueInput.createByString('0 cm')
    newBody = adsk.fusion.FeatureOperations.NewBodyFeatureOperation
    
    #選択された面のあるボディのコンポーネント取得
    comp = face.body.parentComponent
    
    #コンポーネントのフューチャーのオフセットを取得
    offsets = comp.features.offsetFeatures
    
    objs = adsk.core.ObjectCollection.create()
    objs.add(face)
    
    OccActivate(comp.toOcc())
    offsets.add(offsets.createInput(objs, zero, newBody))
    
#adsk.fusion.Component 拡張メソッド
def toOccurrenc(self):
    root = self.parentDesign.rootComponent
    if self == root:
        return None
        
    occs = [occ
            for occ in root.allOccurrencesByComponent(self)
            if occ.component == self]
    return occs[0]

#オカレンスのアクティブ化
def OccActivate(occ):
    app = adsk.core.Application.get()
    des = adsk.fusion.Design.cast(app.activeProduct)
    
    if occ is None:
        des.activateRootComponent()
    else:
        occ.activate()

これで、当初予定していた動きになるようになりました。
まだまだ問題山積みです。オカレンスが。