C#ATIA

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

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の内包表記や拡張メソッド・プロパティなんかも、効率良く
感じました。

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