C#ATIA

↑タイトル詐欺 主にFusion360API 偶にCATIA V5 VBA(絶賛ネタ切れ中)※記載されている内容は個人の意見であり、所属する団体等を代表する意見では御座いません・・・・よ!

難癖

忙しいです・・・はい。
ブログ更新する暇がないのも確かですが、書くためのネタをやっている暇がないです。

Fusion360APIも以前ほど書いていないので、何か作ろうとは思いつつあまりボリュームの
あるものは書くのがしんどいので軽めのテーマを探してはいるものの見つからない。

で、世間のおまえらが昨今Fusion360APIにどの様に向き合っているか検索しつつ難癖つけようじゃねぇか!
(杉本ラララ風)


運の悪いターゲットはこちらです。
(悪意はないです。誤った情報発信のままでは誤解される方が出てくるかなと思い・・・)
【2025】Autodesk FusionのAPIでできることとは?導入手順とMCP連携のコツも紹介 | DX/AI研究所

>設計変更から図面の再出力までを自動で処理でき、
図面作成はAPIでは出来ないのですが、更新ボタンは・・・テキストコマンドだったら出来るかもしれないです。
再出力・・・図面の唯一公開されているAPIはPDF出力なのでそれなら出来るかな?

>代表的なものには以下が挙げられます。
確かにそうだけど、デバッグ出来ない環境での開発は無理ですよ。実質VSCode一択。
有志の方がPyCharm用のプラグインを公開していて、デバッグも出来ていました。
まだあるのかな?

思ったより突っつくところなかったですね。


AIのお陰で以前よりFusion360APIに取り組む人が増えたような気がしてます。

メッシュをサーフェスに変換する2

こちらの続きです。
メッシュをサーフェスに変換する - C#ATIA
正直、続きを書くと思っていませんでした。

キッカケはこちらです。
https://forums.autodesk.com/t5/fusion-ri-ben-yuforamu/10000wo-chaoeru-san-jiaoga-hanmaretamesshudetawosoriddomoderunishitai/m-p/13832881/highlight/true#M60583
記載した内容の類似の処理をします。

サンプルデータはこちらからお借りしました。
https://grabcad.com/library/porsche-brembo-18z-front-caliper-scan-1
ブレンボのブレーキキャリパーです。ブレーキパッド外してくれていれば、もっと良い状態で
3Dスキャン出来ていそうな気もしますが・・・

まず、3Dスキャンデータはどれも重すぎます。まず面の数をこんな条件で減らしておきます。

さっそくコードです。

# Fusion360API Python script
# https://grabcad.com/library/porsche-brembo-18z-front-caliper-scan-1

import traceback
import adsk
import adsk.core as core
import adsk.fusion as fusion
import math
import time

def run(context):
    ui: core.UserInterface = None
    try:
        app: core.Application = core.Application.get()
        ui = app.userInterface

        msg: str = "Select MeshBody"
        selFilter: str = "MeshBodies"
        sel: core.Selection = select_ent(msg, selFilter)
        if not sel:
            return

        t = time.time()
    
        mesh: fusion.MeshBody = sel.entity
        angle: float = 2

        meshs = exec_meshSeparate(mesh, angle)
        for m in meshs:
            adsk.doEvents()
            try:
                exec_meshConvert(m, True) 
                app.log(f" ->{m.name}: Prismatic-OK")

            except:
                try:
                    exec_meshConvert(m, False)
                    app.log(f" ->{m.name}: Faceted-OK")
                except:
                    app.log(f" ->{m.name}: NG")
                
        app.log(f"Done: {time.time() - t}s")

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


def exec_meshConvert(
        mesh: fusion.MeshBody,
        isPrismatic: bool,
) -> list[fusion.MeshBody]:

    comp: fusion.Component = mesh.parentComponent

    meshConvFeats: fusion.MeshConvertFeatures = comp.features.meshConvertFeatures
    meshInputs: fusion.MeshConvertFeatureInput = meshConvFeats.createInput(
        [mesh]
    )
    if isPrismatic:
        meshInputs.meshConvertMethodType = fusion.MeshConvertMethodTypes.PrismaticMeshConvertMethodType
    else:
        meshInputs.meshConvertMethodType = fusion.MeshConvertMethodTypes.FacetedMeshConvertMethodType

    meshInputs.meshConvertOperationType = fusion.MeshConvertOperationTypes.ParametricFeatureMeshConvertOperationType
    
    meshConvFeats.add(meshInputs)


def exec_meshSeparate(
        mesh: fusion.MeshBody,
        angle: float,
) -> list[fusion.MeshBody]:

    comp: fusion.Component = mesh.parentComponent

    # 処理前のMeshのToken
    ents: list[str] = [m.entityToken for m in comp.meshBodies]

    if mesh.faceGroups.count < 2:
        # 面グループ
        fgFeats: fusion.MeshGenerateFaceGroupsFeatures = comp.features.meshGenerateFaceGroupsFeatures
        fgInput: fusion.MeshGenerateFaceGroupsFeatureInput = fgFeats.createInput(mesh)
        fgInput.angleThreshold = core.ValueInput.createByReal(math.radians(angle))
        fgFeats.add(fgInput)

    # 分離
    separateFeats: fusion.MeshSeparateFeatures = comp.features.meshSeparateFeatures
    separateInput: fusion.MeshSeparateFeatureInput = separateFeats.createInput(mesh)
    separateInput.isKeepBody = True
    separateInput.meshSeparateType = fusion.MeshSeparateTypes.FaceGroupMeshSeparateType
    separateFeats.add(separateInput)

    return [m for m in comp.meshBodies if not m.entityToken in ents]


def select_ent(
    msg: str,
    filter: str
) -> core.Selection:

    try:
        app: core.Application = core.Application.get()
        ui: core.UserInterface = app.userInterface
        sel = ui.selectEntity(msg, filter)
        return sel
    except:
        return None

色々と悩んだ末にこんな感じになりましたが、ん~使い道があるものかな?

まず、面グループをチェックします。
面グループはSTL等をインポートした状態でも1個はあります。
手動で面グループを作成していない場合は、面グループ化を行い(角度5度の高速)
面グループ化されている場合は、そのまま分離処理に入ります。
但し、面グループ化は手動で行う事をお勧めします。
一つはどの様な状態で変換するかを事前に確認出来る事は、大きなメリットです。
もう一つは、何故かAPIで処理した方が処理が遅いように感じるからです。

その後サーフェスへの変換処理を行いますが、プリズマティックでチャレンジし
失敗時はファセットで変換します。(個人ライセンスの場合は恐らく全てファセットになると思います)

実際に処理後はこのようになります。

プリズマティックで成功するとそれっぽい仕上がりなんですけどね・・・
正直なところ時間はそれなりにかかりますね。事前に面グループ化した状態で321秒でした。
お役に立つかな?

毛を生やす5

こちらの続きです。
毛を生やす4 - C#ATIA
え~今回は毛を生やしません。

前回のサーフェスを作る・作らないは、その面が該当するボディにとっての外側・内側を
判断し外側と判定した面の場合のみ作っていました。
(そもそも外側・内側の境界って何なのかな?)

これをボディ丸ごと処理するようにしたものがこちらです。

# Fusion360API Python script

# サンプルデータ
# https://grabcad.com/library/random-part-13

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

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

_selFace: core.SelectionCommandInput = None

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


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)

            # cmd.isOKButtonVisible = False

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

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

            inputs: core.CommandInputs = cmd.commandInputs

            global _selFace
            _selFace = inputs.addSelectionInput(
                "_selFaceId",
                "ボディ",
                "外側のみ抜き出すボディを選んで!",
            )
            _selFace.addSelectionFilter(core.SelectionCommandInput.Bodies)

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


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

    def notify(self, args: core.CommandEventArgs):
        args.isValidResult = True
    
        body: fusion.BRepBody = _selFace.selection(0).entity
        outerBody: fusion.BRepBody = get_shell_faces(body)
        if not outerBody:
            return
        
        comp: fusion.Component = body.parentComponent
        des: fusion.Design = comp.parentDesign

        baseFeat: fusion.BaseFeature = None
        if des.designType == fusion.DesignTypes.ParametricDesignType:
            baseFeat = comp.features.baseFeatures.add()

        bodies: fusion.BRepBodies = comp.bRepBodies
        if baseFeat:
            baseFeat.startEdit()
            bodies.add(outerBody, baseFeat)
            baseFeat.finishEdit()
        else:
            bodies.add(outerBody, baseFeat)


def get_shell_faces(
        body: fusion.BRepBody,) -> fusion.BRepBody:

    comp: fusion.Component = body.parentComponent
    tmpFeat: fusion.TemporaryBRepManager = fusion.TemporaryBRepManager.get()
    
    # findBRepUsingRayの結果が自身のBodyとなる結果が、nodesの30%以上であれば内側面
    outerBody: fusion.BRepBody = None
    face: fusion.BRepFace = None
    for face in body.faces:
        meshCalculator: fusion.TriangleMeshCalculator = face.meshManager.createMeshCalculator()
        meshCalculator.maxAspectRatio = 1
        meshCalculator.maxSideLength = 0.1 if face.area < 1 else face.area * 0.1
        triMesh: fusion.TriangleMesh = meshCalculator.calculate()

        nodes = list(triMesh.nodeCoordinates)
        normals = list(triMesh.normalVectors)
        limit = int(len(nodes) * 0.3)

        innerPoints: int = 0

        # UsingRayで自身のボディと交差するかチェック
        for p, v in zip(nodes,normals):
            v2: core.Vector3D = v.copy()
            v2.scaleBy(0.01)
            p2: core.Point3D = p.copy()
            p2.translateBy(v2)
            hitBodies = comp.findBRepUsingRay(
                p2, 
                v, 
                fusion.BRepEntityTypes.BRepBodyEntityType,
                -1,
                False,
                None,
            )
            if hitBodies.count < 1:
                continue
            for hitBody in hitBodies:
                if hitBody.entityToken == body.entityToken:
                    innerPoints += 1
                    break
            
            if innerPoints > limit:
                break

        if not innerPoints > limit:
            # コピー
            if outerBody:
                tmpFeat.booleanOperation(
                    outerBody,
                    tmpFeat.copy(face),
                    fusion.BooleanTypes.UnionBooleanType,
                )
            else:
                outerBody = tmpFeat.copy(face)

    return outerBody


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

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


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()))

前回のデータで試すとこんな感じです。

久々にこうしておきます。

"これはいったい何に使うの?" と思われた方には役に立たないでしょうね。

毛を生やす4

こちらの続きです。
毛を生やす3 - C#ATIA

修正したものも基本的に面を選択し、毛を生やします。
公平さを保つため、こちらのデータをお借りしました。
(パッと見て、欲しいような形状をしていました・・・モデリングも面倒だったので)
https://grabcad.com/library/random-part-13

面を選択前はこんな感じです。

分かりやすく大きな面を選択するとこんな感じです。

毛量が少ないのですが、意図的です。(僕の頭を指さすな)

今回は毛を生やす以外にサーフェスも作成しています。

実は場所によってはサーフェスを作らない部分もあります。

ここまで毛を生やしてきましたが、最初の目的はMeshManagerで得られる法線が常に外向きなのか?
どうかを確認したかったためです。また、作成されるMeshの頂点をコントロールすることが出来るか?
も調べたかったための物でした。

今回の物はMeshManagerで得られる頂点と法線を利用し、Component.findBRepUsingRayを実行し
交差するボディを探し出しています。
Help

最適な値が幾つかが手探り中なのですが、頂点の三割以下の頂点で自身のボディを交差していない
場合のみサーフェスを作成しています。
例えば、この面の場合は全ての頂点で交差するBodyがありません。その場合はサーフェスを作成しています。

逆にこちらの面では、全ての頂点で自身のBodyと交差しています。その場合はサーフェスを作成していません。

"一体、何が目的なのか?" のお話は次回以降にしましょう。

毛を生やす3

こちらの続きです。
毛を生やす2 - C#ATIA

んー今日も時間が無い。
ちょっと別の処理を行っていたのですが、思うような結果が得られず
突貫で確認するために直したコードの為、関数名がふさわしくないです・・・。
何をしているのかは後日。

# Author-
# Description-
# Fusion360API Python script

# サンプルデータ
# https://grabcad.com/library/random-part-13

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

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

_selFace: core.SelectionCommandInput = None

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


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

    def notify(self, args: core.CommandCreatedEventArgs):
        core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers
            cmd: core.Command = core.Command.cast(args.command)

            cmd.isOKButtonVisible = False

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

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

            inputs: core.CommandInputs = cmd.commandInputs

            global _selFace
            _selFace = inputs.addSelectionInput(
                "_selFaceId",
                "面",
                "毛を生やす面を選んで!",
            )
            _selFace.addSelectionFilter(core.SelectionCommandInput.Faces)

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


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

    def notify(self, args: core.CommandEventArgs):
        core.Application.get().log(args.firingEvent.name)
    
        face: fusion.BRepFace = _selFace.selection(0).entity
        outerBody: fusion.BRepBody = get_shell_faces(face)
        if not outerBody:
            return
        
        comp: fusion.Component = face.body.parentComponent
        des: fusion.Design = comp.parentDesign

        baseFeat: fusion.BaseFeature = None
        if des.designType == fusion.DesignTypes.ParametricDesignType:
            baseFeat = comp.features.baseFeatures.add()

        bodies: fusion.BRepBodies = comp.bRepBodies
        if baseFeat:
            baseFeat.startEdit()
            bodies.add(outerBody, baseFeat)
            baseFeat.finishEdit()
        else:
            bodies.add(outerBody, baseFeat)


def get_shell_faces(
        face: fusion.BRepFace,) -> fusion.BRepBody:

    comp: fusion.Component = face.body.parentComponent
    tmpMgr: fusion.TemporaryBRepManager = fusion.TemporaryBRepManager.get()

    # findBRepUsingRayの結果が自身のBodyとなる結果が、nodesの30%以上であれば内側面
    outerBody: fusion.BRepBody = None
    meshCalculator: fusion.TriangleMeshCalculator = face.meshManager.createMeshCalculator()
    meshCalculator.maxAspectRatio = 1
    meshCalculator.maxSideLength = 0.1 if face.area < 1 else face.area * 0.1
    triMesh: fusion.TriangleMesh = meshCalculator.calculate()

    nodes = list(triMesh.nodeCoordinates)
    normals = list(triMesh.normalVectors)
    limit = int(len(nodes) * 0.3)

    skt: fusion.Sketch = comp.sketches.add(comp.xYConstructionPlane)
    skt.isComputeDeferred = True
    skt.arePointsShown = False
    for p1, v in zip(nodes, normals):
        p2: core.Point3D = p1.copy()
        p2.translateBy(v)

        skt.sketchCurves.sketchLines.addByTwoPoints(p1, p2)
    skt.isComputeDeferred = False

    innerPoints: int = 0

    # UsingRayで自身と交差するかチェック
    for p, v in zip(nodes,normals):
        v2: core.Vector3D = v.copy()
        v2.scaleBy(0.01)
        p2: core.Point3D = p.copy()
        p2.translateBy(v2)
        hitBodies = comp.findBRepUsingRay(
            p2, 
            v, 
            fusion.BRepEntityTypes.BRepBodyEntityType,
            -1,
            False,
            None,
        )
        if hitBodies.count < 1:
            continue
        for hitBody in hitBodies:
            if hitBody.entityToken == face.body.entityToken:
                innerPoints += 1
                break
        
        if innerPoints > limit:
            break

    _app.log(f"{limit}: {innerPoints} - {innerPoints > limit}-{face.area}")
    if not innerPoints > limit:
        # コピー
        if outerBody:
            tmpMgr.booleanOperation(
                outerBody,
                tmpMgr.copy(face),
                fusion.BooleanTypes.UnionBooleanType,
            )
        else:
            outerBody = tmpMgr.copy(face)

    return outerBody


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

    def notify(self, args: core.CommandEventArgs):
        core.Application.get().log(args.firingEvent.name)
        adsk.terminate()


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()))

毛を生やす2

こちらの続きです。
毛を生やす1 - C#ATIA

前回は曲面で行いました。・・・非常に眠かったです。
曲面だとフサフサなのですが、境界線が直線の平面の場合はがっかりです。

ん!僕の頭を見ないで欲しい。

要はMeshManagerを使うと、STLでエクスポートした際の頂点や頂点の法線が
エクスポートすることなく手に入ります。

でも、これではさすがに寂しい。毛量が欲しい。

そこでもう少し調べると、TriangleMeshCalculatorオブジェクトがあり、Mesh生成の条件が
調整できそうです。
Help

そこで、MyExecutePreviewHandlerクラスのnotifyメソッドを修正します。
(数値的にどれぐらいが理想的かわかりませんが)

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

    def notify(self, args: core.CommandEventArgs):
        core.Application.get().log(args.firingEvent.name)

        global _selFace
        face: fusion.BRepFace = _selFace.selection(0).entity

        des: fusion.Design = face.body.parentComponent.parentDesign
        root: fusion.Component = des.rootComponent

        # triMesh: fusion.TriangleMesh = face.meshManager.displayMeshes[0]
        meshCalculator: fusion.TriangleMeshCalculator = face.meshManager.createMeshCalculator()
        meshCalculator.maxAspectRatio = 1.0
        meshCalculator.maxSideLength = 1.0
        triMesh: fusion.TriangleMesh = meshCalculator.calculate()

        nodes = list(triMesh.nodeCoordinates)
        normals = list(triMesh.normalVectors)

        skt: fusion.Sketch = root.sketches.add(root.xYConstructionPlane)
        skt.isComputeDeferred = True
        skt.arePointsShown = False
        for p1, v in zip(nodes, normals):
            p2: core.Point3D = p1.copy()
            p2.translateBy(v)

            skt.sketchCurves.sketchLines.addByTwoPoints(p1, p2)
        skt.isComputeDeferred = False
・・・

これで面を選択すると

おぉ素晴らしい!! 育毛効果抜群じゃないですか!

もちろん曲面だってフサフサのまま。

未来は明るい。

毛を生やす1

時間が無い・・・眠い。

ちょっと思うことがあるので作ったサンプルです。もうたたき台です。

# 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 = []

_selFace: core.SelectionCommandInput = None

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


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

    def notify(self, args: core.CommandCreatedEventArgs):
        core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers
            cmd: core.Command = core.Command.cast(args.command)

            cmd.isOKButtonVisible = False

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

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

            inputs: core.CommandInputs = cmd.commandInputs

            global _selFace
            _selFace = inputs.addSelectionInput(
                "_selFaceId",
                "面",
                "毛を生やす面を選んで!",
            )
            _selFace.addSelectionFilter(core.SelectionCommandInput.Faces)

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


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

    def notify(self, args: core.CommandEventArgs):
        core.Application.get().log(args.firingEvent.name)

        global _selFace
        face: fusion.BRepFace = _selFace.selection(0).entity

        des: fusion.Design = face.body.parentComponent.parentDesign
        root: fusion.Component = des.rootComponent

        triMesh: fusion.TriangleMesh = face.meshManager.displayMeshes[0]
        nodes = list(triMesh.nodeCoordinates)
        normals = list(triMesh.normalVectors)

        skt: fusion.Sketch = root.sketches.add(root.xYConstructionPlane)
        skt.isComputeDeferred = True
        skt.arePointsShown = False
        for p1, v in zip(nodes, normals):
            p2: core.Point3D = p1.copy()
            p2.translateBy(v)

            skt.sketchCurves.sketchLines.addByTwoPoints(p1, p2)
        skt.isComputeDeferred = False


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

    def notify(self, args: core.CommandEventArgs):
        core.Application.get().log(args.firingEvent.name)
        adsk.terminate()


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()))

実行するとダイアログが表示されます。

面を選択すると毛が生えます。

もうフサフサ

フサフサは曲面だけなんですけどね。