C#ATIA

↑タイトル詐欺 主にCATIA V5 の VBA(最近はPMillマクロとFusion360APIが多い)

凸包に挑んでみる6

こちらの続きです。
凸包に挑んでみる5 - C#ATIA

結局あっさり諦め、利用しやすそうな軽めのライブラリを探しました。
Start Small: 3D Convex hull in Python
blenderaddons/chull.py at master · varkenvarken/blenderaddons · GitHub
本来、'blender' 向けのようなのですが、他のライブラリとの依存が無く
このファイル 'chull.py' だけで利用できました。

その為、このスプリクトと同一フォルダ内に 'chull.py' を置いておく必要があります。

#FusionAPI_python test_ConvexHull3D
#Author-kantoku
#Description-新たなスケッチを作成し、ランダムに3Dな点を作成し、3Dな凸包を作成

#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#  
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.

import adsk.core, adsk.fusion, traceback
import random, os, sys
this_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(this_dir)

#thx-varkenvarken
#chttps://michelanders.blogspot.com/2012/02/3d-convex-hull-in-python.html
#https://github.com/varkenvarken/blenderaddons/blob/master/chull.py
import chull

def run(context):
    #Create Point Count
    p_count = 100
    
    ui = None
    try:
        #extension
        chull.Vertex.toPnt = ToPoint3d
        chull.Face.toSkt = ToSketch
        adsk.core.Point3D.toVec = ToCHullVector
        adsk.fusion.Sketch.initSurf = InitSurface
        
        #preparation
        app = adsk.core.Application.get()
        ui = app.userInterface
        des = app.activeProduct
        root = des.rootComponent
        
        #RandomPoint
        skt = root.sketches.add(root.xYConstructionPlane)
        skt.name = 'points'
        CreateRandomPoint(skt, -10.0, 10.0, p_count)
        
        #time
        import time
        t = time.time()
        
        #coordinate array
        pnts = [p.geometry.asArray() for p in skt.sketchPoints]
        del pnts[0] #remove origin point
        
        #edges sketch
        skt = root.sketches.add(root.xYConstructionPlane)
        skt.name = 'edges'
        
        #ConvexHull
        face_count = InitConvexHull(pnts,skt)
        
        #finish
        ui.messageBox('Point Count:{}\nface Count:{}\ntime:{:.2f}s'
            .format(p_count,face_count,time.time()- t))
        
    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))

#ConvexHull
def InitConvexHull(point3d_list,target_sketch):
    if len(point3d_list) < 4: return
    
    #hull
    pnts = [chull.Vector(ary[0],ary[1],ary[2]) for ary in point3d_list]
    hull = chull.Hull(pnts)
    
    #edges
    [f.toSkt(target_sketch) for f in hull.faces]
    
    #faces
    target_sketch.initSurf()
    
    return len(hull.faces)

#RandomPoint
def CreateRandomPoint(skt, low, upp, count):
    pnts = [adsk.core.Point3D.create(
            random.uniform(low,upp),random.uniform(low,upp),random.uniform(low,upp)) 
            for dmy in range(count)]
        
    skt_Pnts = skt.sketchPoints
    [skt_Pnts.add(pnt) for pnt in pnts]

# --- extension method ---
#chull.Vertex
def ToPoint3d(self):
    return adsk.core.Point3D.create(
        self.v.x,self.v.y,self.v.z)

#chull.Face
def ToSketch(self, skt):
    #Vertex
    pnts = skt.sketchPoints
    ps = [pnts.add(p.toPnt()) for p in self.vertex]
    
    #edge
    lins = skt.sketchCurves.sketchLines
    edges =[(ps[0],ps[1]),(ps[1],ps[2]),(ps[2],ps[0])]
    [lins.addByTwoPoints(p0,p1) for p0,p1 in edges]
    
#adsk.core.Point3D
def ToCHullVector(self):
    ary = self.asArray()
    return chull.Vector(ary[0],ary[1],ary[2])
    
#adsk.fusion.Sketch
def InitSurface(self,tolerance = 0.001):
    if len(self.profiles) < 1:
        return
        
    newBodyOpe = adsk.fusion.FeatureOperations.NewBodyFeatureOperation
    
    comp = self.parentComponent
    feats = comp.features
    
    objs = adsk.core.ObjectCollection.create()
    [objs.add(v) for v in self.profiles]

    patches = feats.patchFeatures
    pats = patches.add(patches.createInput(objs,newBodyOpe))
    
    objs.clear()
    [objs.add(v) for v in pats.bodies]
    
    tol = adsk.core.ValueInput.createByReal(tolerance)
    stitches = feats.stitchFeatures
    stitches.add(stitches.createInput(objs, tol, newBodyOpe))

結果的に、ベクトル演算等細かなことは一切不要でした。
VBAっぽさを消す為、拡張メソッドを作成しポコポコ呼び出して終わりです。

動画は1分程ですが、半分は処理待ち・・・遅いのは面の作成で
凸包の頂点はほぼ一瞬で求まっています。

質問者さんにちゃんとレスしてあげたかったな。

凸包に挑んでみる5

こちらの続きです。
凸包に挑んでみる4 - C#ATIA

いくらか形になったのですが、不要な面まで作成しまっている。
f:id:kandennti:20180920181747p:plain
しかも、たった10個の点なのに、びっくりする程遅い。

こちらをターゲットに取り組んではいるのですが。
Solved: How to create a convex hull by using Python APIs in Fusion 360? - Autodesk Community
プロが書かれているように、結構処理が重いのかなぁ。
凸包の頂点を探し出すこと自体は、無難な処理時間の気がするのですが、
面を作成するのがものすごく遅いです。

何か方法を探さなきゃ・・・。

不要なスケッチ点をマージし削除する

こちらの問題、ひょっとしたらスプリクト一発で解決出来るかな?
と思いました。
DXFの線分を繋げるには - Autodesk Community

こんな感じです。

#FusionAPI_python SketchPointsMerge
#Author-kantoku
#Description-スケッチのマージ可能な端点をマージする 結構危険

import adsk.core, adsk.fusion, traceback
import itertools

def run(context):
    ui = None
    
    #マージするトレランス 単位Cm!!
    tolerance = 0.001
    
    try:
        #準備
        app = adsk.core.Application.get()
        ui = app.userInterface
        
        #選択
        sel = Sel('スケッチを選択してください','Sketches')
        if sel is None: return
        skt = adsk.fusion.Sketch.cast(sel.entity)
        
        #処理前数
        before = len(skt.sketchPoints)
        
        #マージ
        [ExecMerge(p1,p2) 
        for p1,p2 in itertools.combinations(skt.sketchPoints,2) 
        if IsMerge(p1,p2,tolerance)]
        
        #終わり
        ui.messageBox('{}個、点をマージしました'.format(before-len(skt.sketchPoints)))
        
    except:
        if ui:
            ui.messageBox('エラー:\n{}'.format(traceback.format_exc()))

def ExecMerge(p1,p2):
    try:
        p1.merge(p2)
    except:
        pass
    
def IsMerge(p1,p2,tol):
    try:
        geo1 = p1.geometry
        geo2 = p2.geometry
        return geo1.isEqualToByTolerance(geo2, tol)
    except:
        return False
    
#選択
def Sel(msg, selFilter):
    app = adsk.core.Application.get()
    ui  = app.userInterface
    try:
        return ui.selectEntity(msg, selFilter)
    except:
        return None

スプリクト実行後、任意のスケッチ(Treeから)を選択することで
指定したトレランス以内の点同士をマージします。

実行前
f:id:kandennti:20180920131846p:plain

実行後
f:id:kandennti:20180920131855p:plain
70個の点を削除しています。ギターの輪郭付近の点が激減しているのが
画像でもわかります。

点自体をトレランス以内で移動している為、関連する線が動く可能性があります。
又、トレランスを大きくすると変化が大きすぎる為、Fusion360
オペレーションが戻ってこない可能性が高いです!!

でも、質問者さんの投げかけた問題は解決してません・・・。


念のため書いておきます。
うちのサイトはFusion360スプリクトを記載していますが、CATIA関連の方が
圧倒的にアクセスが多いです。(でも、Fusion360スプリクトとPowerMillマクロは続けます)

凸包に挑んでみる4

こちらの続きです。
凸包に挑んでみる3 - C#ATIA

面倒なため非公開ですが、前回のコードを視覚的に確認出来るように修正し
実際に四面体を見ると
f:id:kandennti:20180920112611p:plain
そこそこ良さそうなのですが、何度試してみても点群に対して薄い形状に
なりやすいんです。気のせいかも知れないのですが・・・。

そこで、知恵が無いなりにもう少し良さそうな4点を選択できないものか?
考えてみました。(イラストはあくまでイメージです)

ピンクが点群です。この点群に対して重心を求めます。(白い点)
f:id:kandennti:20180920112723p:plain

点群の中から、重心から一番遠い点を選ぶ(点1)
f:id:kandennti:20180920112731p:plain

点群の中から、点1から一番遠い点を選ぶ(点2)
f:id:kandennti:20180920112740p:plain

点群の中から、点1-点2ラインに対して一番角度のある点を選ぶ(点3)
結果的に線に対しては一番遠い・・・はず。
f:id:kandennti:20180920112746p:plain
(線の色、変わらんで欲しいのに)

点1,点2,点3を通過する仮平面を考え、平面から一番遠い点を選ぶ(点4)
f:id:kandennti:20180920112754p:plain

これであれば、極端に平べったい四面体にならないような
気がしたのですが、どうでしょうか?

#FusionAPI_python test2
#Author-kantoku
#Description-新たなスケッチを作成し、ランダムに3Dな点を作成し、ユニークな4点を取得

import adsk.core, adsk.fusion, traceback
import random
import itertools

def run(context):
    ui = None
    
    #作成するランダムな点の数
    pointcount = 30
    try:
        #準備
        app = adsk.core.Application.get()
        ui = app.userInterface
        des = app.activeProduct
        root = des.rootComponent
        
        #スケッチと点の作成
        skt = root.sketches.add(root.xYConstructionPlane)
        InitRandomPoint(skt, -10.0, 10.0, pointcount)
        
        #4面体頂点取得
        pnts = [p.geometry for p in skt.sketchPoints]
        uniq_pnts = GetUnique4Points(pnts)
        if uniq_pnts is None: return
        
        #パッチ
        skt = root.sketches.add(root.xYConstructionPlane)
        [CreateSketchLine(skt,p1,p2) for p1,p2 in itertools.combinations(uniq_pnts,2)]
        CreatePatchs(root,skt)
        
    except:
        if ui:
            ui.messageBox('エラー:\n{}'.format(traceback.format_exc()))



#ユニークな4点取得
def GetUnique4Points(pnts):
    if len(pnts) < 4:
        return None
    
    #重心
    cog = GetCog(pnts)
    
    #重心から一番遠い点
    p1 = max(pnts,key=lambda p:p.distanceTo(cog))
    
    #p1から一番遠い点
    p2 = max(pnts,key=lambda p:p.distanceTo(p1))
    
    #p1-p2ベクトル
    vec1_2 = p1.vectorTo(p2)
    
    #p1から一番角度の大きい点
    p3 = max(pnts,key=lambda p:vec1_2.angleTo(p1.vectorTo(p)))
    
    #p1,p2,p3を仮平面した際の法線
    normal = vec1_2.crossProduct(p1.vectorTo(p3))
    normal.normalize()
    
    #平面から一番遠い点
    p4 = max(pnts,key=lambda p:abs(normal.dotProduct(p1.vectorTo(p))))
    
    return [p1,p2,p3,p4]

#点群中心
def GetCog(points3D):
    bbox = adsk.core.BoundingBox3D.create(InitZeroPoint3D(),InitZeroPoint3D())
    [bbox.expand(p) for p in points3D]
    
    return InitMidPoint3D(bbox.maxPoint,bbox.minPoint)

#中間点
def InitMidPoint3D(p1,p2):
    ary1 = p1.asArray()
    ary2 = p2.asArray()
    pos = [(a1+a2)*0.5 for a1,a2 in zip(ary1,ary2)]
    
    return adsk.core.Point3D.create(pos[0],pos[1],pos[2])

#ゼロ点
def InitZeroPoint3D():
    return adsk.core.Point3D.create(0,0,0)

#ランダムな点の作成
def InitRandomPoint(skt, low, upp, count):
    pnts = [adsk.core.Point3D.create(
            random.uniform(low,upp),random.uniform(low,upp),random.uniform(low,upp)) 
            for dmy in range(count)]
        
    skt_Pnts = skt.sketchPoints
    [skt_Pnts.add(pnt) for pnt in pnts]
    
#2点間線分
def CreateSketchLine(skt,p1,p2):
    lines = skt.sketchCurves.sketchLines
    lines.addByTwoPoints(p1,p2)

#パッチ
def CreatePatchs(root,skt): 
    profs = lst2objCollection(skt.profiles)
    patches = root.features.patchFeatures
    newBodyFeatureOpe = adsk.fusion.FeatureOperations.NewBodyFeatureOperation
    patches.add(patches.createInput(profs,newBodyFeatureOpe))
    
#リスト→objCollection
def lst2objCollection(lst):
    ents = adsk.core.ObjectCollection.create()
    [ents.add(ent) for ent in lst]    
    return ents

#開発用
def dumpPoint(p):
    print("{:.3f},{:.3f},{:.3f}".format(p.x *10,p.y*10,p.z *10))
    
def dumpPoints(ps):
    [dumpPoint(p) for p in ps]

同じ点群に対して前回と今回のもので処理した結果です。
f:id:kandennti:20180920112808p:plain
黄色と体積の左は前回、緑と右が今回です。
2倍弱ぐらいにはなりました。

よく考えたら、遠い点ばかり選択するから平べったくなるのかも知れない。

凸包に挑んでみる3

こちらの続きです。
凸包に挑んでみる2 - C#ATIA

2D凸包のスタートは1本の直線からなのですが、3Dは4面体からが正攻法のようです。
その為、出来る限りユニークで直線に並ばない4点が欲しいのですが、
具体的なアルゴリズムがわかりません。

色々と試した結果、こんな感じで良いものかな?と・・・
・XYZの最大・最小を持つ点を取得(最大6点)
・6点中同一の点を除外する
・4点の組み合わせを作り、4点間の合計距離が一番大きい組み合わせを取得
と考えました。 こちら

#FusionAPI_python test
#Author-kantoku
#Description-新たなスケッチを作成し、ランダムに3Dな点を作成し、ユニークな4点を取得

import adsk.core, adsk.fusion, traceback
import random

def run(context):
    ui = None
    
    #作成するランダムな点の数
    pointcount = 100

    try:
        #準備
        app = adsk.core.Application.get()
        ui = app.userInterface
        des = app.activeProduct
        root = des.rootComponent
        
        #スケッチと点の作成
        skt = root.sketches.add(root.xYConstructionPlane)
        InitRandomPoint(skt, -10.0, 10.0, pointcount)
        
        pnts = [adsk.core.Point3D.cast(p.geometry) for p in skt.sketchPoints]
        uniq_pnts = GetUnique4Points(pnts)
        dumpPoints(uniq_pnts)

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

#ユニークな4点取得
def GetUnique4Points(pnts):
    lst = [ min(pnts,key=lambda p:p.x),
            max(pnts,key=lambda p:p.x),
            min(pnts,key=lambda p:p.y),
            max(pnts,key=lambda p:p.y),
            min(pnts,key=lambda p:p.z),
            max(pnts,key=lambda p:p.z)] 
    
    lst2 = []
    for p in lst:
        if not p in lst2:
            lst2.append(p)
            
    import itertools
    return max(itertools.combinations(lst2,4),key=lambda p:SumDist(p))

#点リストの合計距離
def SumDist(pnts):
    import itertools
    return sum(p1.distanceTo(p2) for p1,p2 in 
        itertools.combinations(pnts,2))

#ランダムな点の作成
def InitRandomPoint(skt, low, upp, count):
    pnts = [adsk.core.Point3D.create(
            random.uniform(low,upp),random.uniform(low,upp),random.uniform(low,upp)) 
            for dmy in range(count)]
        
    skt_Pnts = skt.sketchPoints
    [skt_Pnts.add(pnt) for pnt in pnts]
    return

def dumpPoint(p):
    print("{:.3f},{:.3f},{:.3f}".format(p.x *10,p.y*10,p.z *10))
    
def dumpPoints(ps):
    [dumpPoint(p) for p in ps]

で、3点が直線になっていないか? のチェックを忘れている・・・。

重複点を除外する為にセットを利用したかったのですが、駄目なんですね。
どうやらハッシュメソッドを持っていないとNGらしいです。(まぁそうでしょう・・・)
重複点を削除した時点で4点以下の場合どうしよう・・・。

凸包に挑んでみる2

こちらの続きです。
凸包に挑んでみる1 - C#ATIA

昨日のイマイチな処理、使用すべきベクトルが間違っていたと言うお粗末な結末。
ある程度修正したので、利用価値があるものかどうか不明ながら公開。

#FusionAPI_python ConvexHull2D
#Author-kantoku
#Description-新たなスケッチを作成し、ランダムに2Dな点を作成し、凸包する

import adsk.core, adsk.fusion, traceback
import random

def run(context):
    ui = None
    
    #作成するランダムな点の数
    pointcount = 100

    try:
        #準備
        app = adsk.core.Application.get()
        ui = app.userInterface
        des = app.activeProduct
        root = des.rootComponent
        
        #スケッチと点の作成
        skt = root.sketches.add(root.xYConstructionPlane)
        InitRandomPoint(skt, -10.0, 10.0, pointcount)
        
        #時間測定開始
        import time
        t = time.time()
        print('-- start --')
        
        #凸包
        ConvexHullEdges = ExeConvexHull(skt.sketchPoints)
        [print("({:.3f},{:.3f})-({:.3f},{:.3f})"
            .format(b[0].x *10 ,b[0].y*10,b[1].x *10 ,b[1].y*10)) for b in ConvexHullEdges]
        
        [CreateSketchLine(skt,Vec2dtoPnt3d(v1) ,Vec2dtoPnt3d(v2)) for v1,v2 in ConvexHullEdges]
        
        #終了
        t = time.time()- t
        ui.messageBox(' Points Count:{}\n ConvexHull Edges Count:{}\n time:{:.2f}s'
            .format(pointcount,len(ConvexHullEdges),t))
        
    except:
        if ui:
            ui.messageBox('エラー:\n{}'.format(traceback.format_exc()))

#凸包処理
def ExeConvexHull(points):
    #スケッチ点群をVector2D化
    vecs = [adsk.core.Vector2D.create(p.geometry.x, p.geometry.y)
            for p in points]
                
    #X方向のMinMax取得
    vec_min = min(vecs,key=lambda v:v.x)
    vec_max = max(vecs,key=lambda v:v.x)
    
    #半分づつ処理
    boundarys = []
    boundarys = GetConvexList(vec_min,vec_max,vecs,boundarys)
    boundarys = GetConvexList(vec_max,vec_min,vecs,boundarys)
    
    return boundarys

#凸包となる点リスト作成 - 再帰やめたい
def GetConvexList(v1, v2, vecs, boundarys):
    #始点終点あh除外
    if v1 in vecs:
        vecs.remove(v1)
    if v2 in vecs:
        vecs.remove(v2)
    
    #v1, v2の直交ベクトル
    vec1_2 = v2.copy()
    vec1_2.subtract(v1)
    vec_crs = adsk.core.Vector2D.create(vec1_2.y * -1, vec1_2.x)
    
    #v1, v2の外側検索
    check_lst = []
    for v in vecs:
        vv = v.copy()
        vv.subtract(v1)
        if vec_crs.dotProduct(vv) < 0:
            check_lst.append((v,vec1_2.angleTo(vv)))

    if len(check_lst) < 1:
        #v1, v2の組み合わせでOK
        boundarys.append((v1, v2))
        return boundarys
    
    #一番外の点検索
    max_pnt,ang = max(check_lst,key=lambda p:p[1])
    
    vecs = [v for (v,a) in check_lst]
    boundarys = GetConvexList(v1,max_pnt,vecs,boundarys)
    boundarys = GetConvexList(max_pnt,v2,vecs,boundarys)
    
    return boundarys

#Point2D又はVecter2DからZ0のPoint3Dを作成
def Vec2dtoPnt3d(v):
    pnt = adsk.core.Point3D.create(v.x,v.y,0)
    return pnt

#2点間戦分
def CreateSketchLine(skt,p1,p2):
    lines = skt.sketchCurves.sketchLines
    line = lines.addByTwoPoints(p1,p2)

#ランダムな点の作成
def InitRandomPoint(skt, low, upp, count):
    pnts = [adsk.core.Point3D.create(
            random.uniform(low,upp),random.uniform(low,upp),0) 
            for dmy in range(count)]
        
    skt_Pnts = skt.sketchPoints
    [skt_Pnts.add(pnt) for pnt in pnts]
    return

凸包となる点を検索するアルゴリズムは、こちらの "ギフト包装法" では無く
https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%95%E3%83%88%E5%8C%85%E8%A3%85%E6%B3%95

こちらで説明されている "Quickhull" にしたつもりです。(直感的に速そうだったので)
http://asura.iaigiri.com/OpenGL/gl50.html

Fusion360のスケッチは2Dでは無く3Dの為、どうしようかと悩みました。
処理上、内外積を使う必要があり、内積はまぁ構わないのですが
外積は次元によって答えが変わってきます。(2Dと3Dしか知らないですが・・・)
結局、効率無視で2Dとして処理し、最後に3Dの線(スケッチの線)を作成しました。

Fusion360APIは、Vector3DとVector2Dの両方用意されているのですが、
Vector3Dには外積(crossProductメソッド)が用意されているのに
Help

Vector2Dには無いんです・・・。何でだろう?
Help


ついでに愚痴なのですが、Vector3D(2Dも同様)のメソッドが破壊的か?非破壊的か?が、
かなり迷いました。
・破壊的 - ベクトルそのものが変化してしまうもの
 add(和),subtract(差),normalize(単位化),scaleBy(スケール),transformBy(変換)
・非破壊的 - 戻り値で新たなベクトルが返ってくるもの
 copy(複製 ←深いよ),crossProduct(外積)
こうして見ると外積だけか・・・個人的には全て非破壊的にして欲しい。

凸包に挑んでみる1

こちらの続きです。
スケッチにランダムな点を作成する - C#ATIA

ランダムに描かれた点を元に、凸包となる線を描きます。
f:id:kandennti:20180919003112p:plain
一瞬、上手く行ったように見えたのですが、左下付近の境界がNGです。