C#ATIA

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

MeshBodyの面を選択した風に表示する

久々にFusion360です。

こちらに挑戦してみました。
Solved: Use Selection Input to select a single triangle in a mesh? - Autodesk Community
MeshBodyの小さな一面をAPIで選択出来ないか? と言った内容です。
実際に面を選択させるのではなく、選択したように見えれば良いようです。

最初は全ての頂点の中から、クリックした位置に近い3点で面を作れば
それっぽく出来るだろう と思っていたのですが、精度が非常に悪い
又は、一つの面とならない3点が選ばれたりしたため、もっとまじめに
考えました。


Fusion360API的にはMeshBody(インポートされたSTL等)は
頂点と頂点の組み合わせの情報しか提供していません。
つまり面の情報そのものは提供しない為、作り出す必要があります。
Fusion 360 Help

幸いこちらで、それらの処理を行った事が有りました。
Re: Silhouette of a mesh body - Autodesk Community

全ての三角面を作り、クリックした点に一番近いものを選び出そうとも
思ったのですが、処理時間が長く断念。
時間のかかる一番の原因は全ての三角面を作っている為です。

そこで思い付いたのが、次の方法です。

既に、三角面を作る頂点の組み合わせは分かっています。
面を作らずに、クリックした位置が含まれる3個の頂点の組み合わせを
見つける事が出来れば、作成する面の数を最小限(通常は1個)に
すること出来そうです。

その為のアルゴリズムを探した所、こちらが見つかりました。
点と三角形の当たり判定( 内外判定 )
昔から色々と教わっているサイトです。

6個のベクトルを作成し、外積3回、内積2回で判断出来るようです。

で、作成したものがこちらです。

# Fusion360API Python script

import traceback
import adsk
import adsk.core as core
import adsk.fusion as fusion


_app: core.Application = None
_ui: core.UserInterface = None
_handlers = []

CMD_INFO = {
    'id': 'kantoku_test',
    'name': 'test',
    'tooltip': 'test'
}

_meshIpt: core.SelectionCommandInput = None

class MyCommandCreatedHandler(core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: core.CommandCreatedEventArgs):
        try:
            global _handlers
            cmd: core.Command = core.Command.cast(args.command)
            inputs: core.CommandInputs = cmd.commandInputs

            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            onExecutePreview = MyExecutePreviewHandler()
            cmd.executePreview.add(onExecutePreview)
            _handlers.append(onExecutePreview)

            global _meshIpt
            _meshIpt = inputs.addSelectionInput(
                '_meshIptId',
                'mesh',
                'Select Mesh',
            )
            _meshIpt.addSelectionFilter(core.SelectionCommandInput.MeshBodies)

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


class MyExecutePreviewHandler(core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: core.CommandEventArgs):
        sel: core.Selection = _meshIpt.selection(0)

        meshBody: fusion.MeshBody = sel.entity
        point: core.Point3D = sel.point

        _meshIpt.clearSelection()

        meshBody.opacity = 0.5
        cgBodies = get_triangle_bodies(meshBody, point)
        [_meshIpt.addSelection(b) for b in cgBodies]


class MyCommandDestroyHandler(core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: core.CommandEventArgs):
        adsk.terminate()


def get_triangle_bodies(
    mesh: fusion.MeshBody,
    point: core.Point3D,
) -> list[fusion.BRepBody]:

    tmpMgr: fusion.TemporaryBRepManager = fusion.TemporaryBRepManager.get()

    # *******
    def init_lines(
        pnts: list[core.Point3D],
    ) -> list[core.Line3D]:

        points = list(pnts)
        points.append(pnts[0])
        
        lineLst = []
        for p1, p2 in zip(points, points[1:]):
            try:
                line = core.Line3D.create(p1, p2)
                lineLst.append(line)
            except:
                pass

        return lineLst


    def init_pointSets(
        idxs: list,
        nodePoints: list[core.Point3D],
    ) -> tuple:

        return tuple([nodePoints[i] for i in idxs])


    def init_wireBodies(
        crvs: list,
    ) -> list[fusion.BRepBody]:

        try:
            wireBody, _ = tmpMgr.createWireFromCurves(crvs)
            return wireBody
        except:
            pass


    def init_tri_faceBodies(
        wireBodies: list[fusion.BRepBody],
    ) -> list[fusion.BRepBody]:

        triBodies = []
        for b in wireBodies:
            try:
                triBodies.append(tmpMgr.createFaceFromPlanarWires([b]))
            except:
                pass

        return triBodies


    def is_inner(
        point: core.Point3D,
        pointSet: tuple[core.Point3D],
    ) -> bool:

        vecAB: core.Vector3D = pointSet[1].vectorTo(pointSet[0])
        vecBP: core.Vector3D = point.vectorTo(pointSet[1])
        vecBC: core.Vector3D = pointSet[2].vectorTo(pointSet[1])
        vecCP: core.Vector3D = point.vectorTo(pointSet[2])
        vecCA: core.Vector3D = pointSet[0].vectorTo(pointSet[2])
        vecAP: core.Vector3D = point.vectorTo(pointSet[0])

        vec1: core.Vector3D = vecAB.crossProduct(vecBP)
        vec2: core.Vector3D = vecBC.crossProduct(vecCP)
        vec3: core.Vector3D = vecCA.crossProduct(vecAP)

        dot12: float = vec1.dotProduct(vec2)
        dot13: float = vec1.dotProduct(vec3)

        return True if dot12 > 0 and dot13 > 0 else False


    def draw_cg_bodies(
        comp: fusion.Component,
        bodyLst: list,
    ) -> list:

        cgGroup: fusion.CustomGraphicsGroup = comp.customGraphicsGroups.add()

        cgBodies = [cgGroup.addBRepBody(b) for b in bodyLst]

        blue = fusion.CustomGraphicsSolidColorEffect.create(
            core.Color.create(0,0,120,255)
        )
        for b in cgBodies:
            b.color = blue 

        return  cgBodies

    # *******
    triMesh: fusion.TriangleMesh = mesh.displayMesh

    nodeIndices = triMesh.nodeIndices
    triIndices = [nodeIndices[i: i + 3] for i in range(0, len(nodeIndices), 3)]

    nodePoints = [p for p in triMesh.nodeCoordinates]

    pointSets = [init_pointSets(idxs, nodePoints) for idxs in triIndices]

    triPointSets = [pnts for pnts in pointSets if is_inner(point, pnts)]
    if len(triPointSets) < 1: return

    lineSets = [init_lines(triPointSet) for triPointSet in triPointSets]
    triWires = [init_wireBodies(lines) for lines in lineSets]
    triBodies = init_tri_faceBodies(triWires)
    cgBodies = draw_cg_bodies(mesh.parentComponent, triBodies)

    return cgBodies


def run(context):
    try:
        global _app, _ui
        _app = core.Application.get()
        _ui = _app.userInterface

        cmdDef: core.CommandDefinition = _ui.commandDefinitions.itemById(
            CMD_INFO['id']
        )

        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                CMD_INFO['id'],
                CMD_INFO['name'],
                CMD_INFO['tooltip']
            )

        global _handlers
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        _handlers.append(onCommandCreated)

        cmdDef.execute()

        adsk.autoTerminate(False)

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

選択した三角面と同じ位置にカスタムグラフィックの三角面を作って
表示させています。
長ったらしいですが、実際に処理させているのはget_triangle_bodies関数です。

しかし、問題が有りました。

最初に選択しているものは球体から作ったMeshBodyです。
こちらは狙った通りの面が選択出来ています。

二個目に選択しているものは、円柱から作ったMeshBodyです。
こちらは予想を裏切り、全く異なる面を選択してしまっています。

この二つの大きな違いは、MeshBodyが持っているメッシュグループが
単体か複数かと言う事に気が付くまで時間がかかりました。

最後に円柱のMeshBodyを一つのメッシュグループにし、その後
スクリプトを実行すると狙った動作となります。

何とも中途半端ですな。