こちらの続きです。
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の内包表記や拡張メソッド・プロパティなんかも、効率良く
感じました。
出来れば、もう少しコマンドっぽくしたい所。