C#ATIA

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

Perspective Angle Controller

こちらで公開しているアドインを更新しました。
GitHub - kantoku-code/Fusion360_PerspectiveAngleController: Display a modaless dialog to adjust the perspective angle.
見た目は何も変わってません・・・。

先日のこちらが分かったので、Paletteでも動作するかを
確認する為です。
スタート時BrowserCommandInputに初期値を渡す - C#ATIA

使用するイベントを "Load" -> "DOMContentLoaded" へ変更した為、
以前はダイアログが表示された瞬間にスライダーが
パッと移動していたのですが、表示される前に値を修正している為
違和感が全くなくなりました。


旧ブラウザがいつかのUpdateで削除されると思うので、
対応出来ている状態はかなりの安心感。

スタート時BrowserCommandInputに初期値を渡す

以前からどうやって良いのかわからなかった処理が、やっと
昨夜分かりましたので、記載しておきます。
(上手く行った際、思わず声が出た・・・)


元ネタはこちらです。
パレットに苦しむ2 - C#ATIA
パレットの中身はブラウザなのですが、旧タイプはCEFコンポーネント
(Chromium Embedded Framework ...chromeとかedgeに使われている奴)
ですが、新タイプはQtWebブラウザーコンポーネントです。
そして再三、旧タイプは無くなってしまうので切り替えるように
アナウンスされています。

先程の元ネタでは旧タイプを使用しているのですが、あのまま新タイプに
してしまうと、初期値をパレットに反映することが出来ませんでした。
(python -> javascriptのデータの受け渡しが出来なかったです)

その為、旧タイプから新タイプに切り替えることが出来ずに放置して
いました。・・・当然、後々のことを考えれば良くないです。


BrowserCommandInputも中身はブラウザなのですが、かなり後から
実装されたため旧タイプが無く、新タイプのQtWebブラウザー
コンポーネントです。 つまり初期値の受け渡し方法が分からなく
ここ一週間ほど、悩んでいました。


何せFusion360APIの内容な為、検索してもそれらしきサンプルが
見つからず、自分で試すしか方法がありません。
又、起動時の処理はデバッグが非常に行いにくく、無い知恵絞って
試すのですが、何れも上手く行かず途方に暮れていました。

解決のきっかけは、公式ドキュメントに記載されていました。
(真ん中より少し下に記載あり。ちゃんと読むべき)
Fusion 360 Help
CEFコンポーネントは同期処理されるが、QtWebブラウザーコンポーネント
は非同期処理されるとの事。 最初は "フーン" 程度で完全には理解
出来ませんでした。と言うか、どの様に対処知れば良いのかが
分かりませんでした。

javasprictを理解しきっていないのですが、どうやらコールバック関数や
thenを使えって事の様です。


と言う事で、必要最低限の出来上がったサンプルです。
スクリプト起動時に、アクティブなドキュメント名をBrowserCommandInput
で表示させています。

こちらは、python側です。まぁこちらはこれと言って問題無かったです。

# Fusion360API Python script

import adsk.core
import adsk.fusion
import traceback

_app: adsk.core.Application = None
_ui: adsk.core.UserInterface  = None

_cmdId = 'browserInputTest'
_handlers = []


class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            cmd = adsk.core.Command.cast(args.command)

            # inputs
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            # global _browserIpt
            inputs.addBrowserCommandInput(
                'browserIptId',
                '',
                'index.html',
                300,
                100
            )

            # event
            onDestroy = MyDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            onIncomingFromHTML = MyIncomingFromHTMLHandler()
            cmd.incomingFromHTML.add(onIncomingFromHTML)
            _handlers.append(onIncomingFromHTML)

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


class MyIncomingFromHTMLHandler(adsk.core.HTMLEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.HTMLEventArgs):
        if args.action == 'DOMContentLoaded':
            args.returnData = getActDocName()


class MyDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        eventArgs = adsk.core.CommandEventArgs.cast(args)

        adsk.terminate() 

def getActDocName() -> str:
    global _app
    return _app.activeDocument.name


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

        global _cmdId
        cmdDef = _ui.commandDefinitions.itemById(_cmdId)
        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                _cmdId,
                'Browser Input Test',
                'Browser Input Test'
            )

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

続いて、html側で "index.html" と言うファイル名にしています。
上記のpython(.py)ファイルと同一フォルダに配置しておきます。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Qt Web Browser Test</title>
  </head>
  <body>
    <p id="xxx">** BrowserInput Test **</p>
  </body>
  <script>
    document.addEventListener("DOMContentLoaded", () => {
      let adskWaiter = setInterval(() => {
        console.log("DOMContentLoaded");
        if (window.adsk) {
          console.log("adsk ok");
          clearInterval(adskWaiter);

          let element = document.getElementById("xxx");
          adsk
            .fusionSendData("DOMContentLoaded", "{}")
            .then((data) =>
              element.insertAdjacentHTML("afterend", "<p>" + data + "</p>")
            );
        }
      }, 100);
    });
  </script>
</html>

実行するとこんな無意味なダイアログが表示されます。

赤矢印はhtmlにベタに記載していますが、青矢印はイベントで
後から追記させたアクティブなドキュメント名です。


細々とした(幼児レベルの)覚書です。

・元ネタJeromeBriotさんの物は、"window.onload" でイベントを
拾っていますが、あの方法はあまり良くないとの記述を見つけました。
window.onloadの利用はお勧めしない その理由
その為、addEventListenerを利用するようにしました。

・"addEventListener" する際、 "load" "DomContentLoaded" の
どちらかのイベントを利用するのですが、呼び出しタイミングの
早い "DomContentLoaded" にしました。

元ネタでは "window.adsk" の記載でしたが、"window” は
省略可能だと知っていました。

今回の例では

if (adsk) {

でも上手く処理出来るのですが、上手く行かない時がある為

if (window.adsk) {

とする方が良い事が分かりました。

・元ネタでは

var adskWaiter = setInterval(function () {

になっていますが、アロー関数を利用すると

let adskWaiter = setInterval(() => {

と書けることを知っていました。
(varも良くない!)

・恐らく一発では "adsk" (オブジェクト?) を見つける事が
出来ないのだと思うので、setIntervalでadskを探し出し
発見次第にclearIntervalで止めていると理解しました。

・非同期のpromiseオブジェクトの場合、thenを利用した
メソッドチェーンで処理することがお作法だと教わりました。
【ES6】 JavaScript初心者でもわかるPromise講座 - Qiita



あぁやっと進める。 時間出来たら他のアドインも書き換えなきゃ。

押すと回転する機構

押すと回転する機構ってどんなものだろうか?

業務でアルミのプレートに穴をあけた際(マシニングセンター)の、
面取りを悩んでます。
"マシニングセンターでやりゃ良いじゃん" とは思うのですが、
糸面取りなので機械からおろして、次のプレートを加工している
最中に、ドリルでモミモミして面取ってます・・アホくさいですか?

以前の会社でも、貫通穴の裏側は手作業だったし(世の中には裏面取りを
マシニングセンターで行う為の便利な工具が存在している事は知ってます)、
ボール盤使うのも面倒。(社内の他の者はボール盤で行っている)
そもそも糸面取りをマシニングセンターで行うのって難しくないですかね?
結構大きく取り過ぎちゃったりして・・・。面取りごときでお釈迦も避けたい。

そこで手作業で面取り行えるものを探したのですが、ちょっと物足り
ないんです。イメージ的にはこんなので良いのですが、
カウンターシンク ハンディ タイプ モノタロウ カウンターシンク 【通販モノタロウ】 CS-H
物足りなんです。

可能であれば、これを穴にグッと押し付けてストロークさせると、
先端が回転してくれる・・・と言うのが欲しい。
え?電動のヤツ買えよって?
いや、ここは非電動にこだわりたい。しかし世の中には無さそう・・・。
冒頭の "押すと回転する機構ってどんなものだろうか?" は、そんな
思いからです。

・・・取りあえず、面倒だからリンク先のやつ買おうかな?

マシニングセンタを無料で使う方法は多分ない

こちらを見ていて感じたのですが
cad-kenkyujo.com
恐らく無理ですよ・・・。

ジモティーで探す” については、仮に無償で譲渡してくれる機械は
見つかるかも知れません。
が、間違いなく運送代は受け取り側負担成るはずです。
(譲渡する主な理由は、機材が古く処分代がかかるので
それだったら持って行ってくれ だろうと思います。)

運送依頼する業者も何処でも良いわけでは無く、それが可能な技術を
持っている業者となる為、単に "重い物運べます” と言う業者では
不可能です。

個人でそのような経験が有り、ユニックやらチェーンブロックやらコロ
等を所有していれば別ですが、そんな人はぼぼ居ないかと。
そもそも一人じゃ無理な気がします。
料金について|重量物輸送 機械輸送|重量物マスター陸進運輸|愛知,岐阜,三重,静岡
これは安い方な気もします。



"持っている人を探す" ですが、持っている人は見つかると思います。
但し、使わせるかどうかと言えば、間違いなく使わせないと思います。
ぶつけて壊される可能性がある以上、絶対に使わせないですよ。(うちだって)
”無料で使わせてあげる” の可能性はほぼ無いのですが、
"無料で加工してあげる" はあり得ますね。こちらの方が
可能性は高いと思います。
(材料と工具は持ってきてね と言う条件付きで)



"無料で" は難しいですが "格安で" であれば知っています。
全ての都道府県に有るかどうかは定かでは無いのですが、公的な機関で
"産業技術センター" のようなものが結構な数存在しています。

東京じゃ一般的じゃないので、群馬を例にしてみると
www.tec-lab.pref.gunma.jp
マシニングセンタの使用料は¥2500/h ですよ。
恐らく事前に講習を受けたりする必要はあると思いますが、
それでも格安で。
(機械自体も結構新しいですし、アドバイザー付きのはず)

一年に一度使うかどうか?ぐらいの頻度なら、十分だと思います。

BrowserCommandInputとReact その2

こちらの続きです。
BrowserCommandInputとReact その1 - C#ATIA

前回の "index.html" にPopoversとBootstrap(共にCDN)を追加し
見た目をリッチに。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <script src="https://unpkg.com/@popperjs/core@2"></script>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
      crossorigin="anonymous"
    />
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
      crossorigin="anonymous"
    ></script>

    <title>react_cdn</title>
  </head>

  <body>
    <div id="root"></div>
  </body>

  <script type="text/babel">
    function ListItem(props) {
      return (
        <li
          className="list-group-item list-group-item-action list-group-item-primary"
          data-bs-toggle="tooltip"
          data-bs-placement="top"
          title={props.value}
        >
          {props.value}
          <span className="badge bg-dark rounded-pill">{props.value}</span>
        </li>
      );
    }

    function NamesList(props) {
      const names = props.names;
      return (
        <ul class="list-group">
          {names.map((name) => (
            <ListItem key={name.toString()} value={name} />
          ))}
        </ul>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById("root"));

    // 最初のレスポンスが悪すぎる
    root.render(<NamesList names={[" "]} />);
    root.render(<NamesList names={["  "]} />);
    root.render(<NamesList names={[]} />);

    window.fusionJavaScriptHandler = {
      handle: function (action, data) {
        try {
          switch (action) {
            case "test":
              const names = data.split("@");
              root.render(<NamesList names={names} />);
              break;
          }
        } catch (e) {
          console.log(e);
          console.log("exception caught with command: " + action);
        }
        return "OK";
      },
    };
  </script>
</html>

少しずつですが、結果になりつつあります。
なるほどね。

BrowserCommandInputとReact その1

次の作戦の為にテストです。

表現力を豊かにするため、BrowserCommandInputを使用する事にします。
Fusion 360 Help
BrowserCommandInputは、僕がFusion360APIを取り組み始めて唯一後から追加された
CommandInputsです。
今まで使用した事が無かったのですが、ほぼPaletteと同じの為
"何とかなるだろう" とは感じています。

BrowserCommandInputで表示させるhtml側は、動的な表示が欲しいため
Reactを利用する事にします。・・・理解が不足しまくってますが、
お勉強がてらです。

まずはpython側のスクリプトです。

# Fusion360API Python script

import adsk.core
import adsk.fusion
import traceback

_app: adsk.core.Application = None
_ui: adsk.core.UserInterface  = None

_cmdId = 'browserInputReactTest'
_selIpt: adsk.core.SelectionCommandInput = None
_browserIpt: adsk.core.BrowserCommandInput = None

_handlers = []


class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        try:
            cmd = adsk.core.Command.cast(args.command)
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            global _selIpt
            _selIpt = inputs.addSelectionInput(
                'selIptId',
                'Bodies',
                'Select Body'
            )
            _selIpt.addSelectionFilter(adsk.core.SelectionCommandInput.Bodies)
            _selIpt.setSelectionLimits(0)

            global _browserIpt
            _browserIpt = inputs.addBrowserCommandInput(
                'browserIptId',
                '',
                'index.html',
                300,
                300
            )

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

            onInputChanged = MyCommandInputChangedHandler()
            cmd.inputChanged.add(onInputChanged)
            _handlers.append(onInputChanged)

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


class MyCommandInputChangedHandler(adsk.core.InputChangedEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args: adsk.core.InputChangedEventArgs):
        try:
            global _selIpt
            if args.input != _selIpt:
                return

            names = []
            for idx in range(_selIpt.selectionCount):
                ent = _selIpt.selection(idx).entity
                if ent:
                    names.append(ent.name)

            global _browserIpt
            _browserIpt.sendInfoToHTML(
                'test',
                '@'.join(names)
            )

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


class MyDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()
    def notify(self, args):
        eventArgs = adsk.core.CommandEventArgs.cast(args)

        adsk.terminate() 


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

        global _cmdId
        cmdDef = _ui.commandDefinitions.itemById(_cmdId)
        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                _cmdId,
                'Browser Input React Test',
                'Browser Input React Test'
            )

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

若干、型ヒントが不足していますが、テストの為お許しを。

htmlファイルは "index.html" として同じフォルダ内に設置します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <script
      crossorigin
      src="https://unpkg.com/react@18/umd/react.development.js"
    ></script>
    <script
      crossorigin
      src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
    ></script>
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

    <title>react_cdn</title>
  </head>

  <body>
    <div id="root"></div>
  </body>

  <script type="text/babel">
    function ListItem(props) {
      return <li>{props.value}</li>;
    }

    function NamesList(props) {
      const names = props.names;
      return (
        <ul>
          {names.map((name) => (
            <ListItem key={name.toString()} value={name} />
          ))}
        </ul>
      );
    }

    const root = ReactDOM.createRoot(document.getElementById("root"));

    // 最初のレスポンスが悪すぎる
    root.render(<NamesList names={[" "]} />);
    root.render(<NamesList names={["  "]} />);
    root.render(<NamesList names={[]} />);

    window.fusionJavaScriptHandler = {
      handle: function (action, data) {
        try {
          switch (action) {
            case "test":
              const names = data.split("@");
              root.render(<NamesList names={names} />);
              break;
          }
        } catch (e) {
          console.log(e);
          console.log("exception caught with command: " + action);
        }
        return "OK";
      },
    };
  </script>
</html>

こんな感じの動作です。

環境を作るのが面倒な為、React・Babel共にCDNにしています。


取り組むまでわからなかったのが、幾つか有りました。
今までPaletteを利用していたアドインでは、全てダイアログでアクションを
行い処理させるものばかりでした。
例えば、こちらはボタンを押すアクションで色々とPython側で処理させています。
Fusion360_Small_Tools_for_Developers/Developers_Small_ToolKit at master · kantoku-code/Fusion360_Small_Tools_for_Developers · GitHub
汚すぎるので、細かく見ない方が身のためです。

つまり、ダイアログ->Fusion360の処理、又はダイアログ->Fusion360->ダイアログ
の処理については、incomingFromHTMLイベントで処理出来ます。
https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-0921986D-D97E-4A96-931D-E2C74A63C358
細かなお話は、今回のテーマでは無いため割愛します。


わからなかったのがFusion360でアクションを起こした際、ダイアログの
表示を反映させる方法です。つまりFusion360->ダイアログです。
サンプル等を参考にしたところ、Palette.sendInfoToHTMLメソッドで処理が
可能だと分かりました。
Fusion 360 Help


今回の場合であれば、inputChangedイベントハンドラー内のこちらです。

            _browserIpt.sendInfoToHTML(
                'test',
                '@'.join(names)
            )

第2引数は文字列なのですが、選択されたボディ名のリストを改行で連結して
javascript側に投げたのですが、受け取り側で上手くsplit出来なかった為、
"@" の文字で連結しています。(原因は謎)

受け取り側のjavascriptではこの様にしています。

    window.fusionJavaScriptHandler = {
      handle: function (action, data) {
        try {
          switch (action) {
            case "test":
              const names = data.split("@");
              root.render(<NamesList names={names} />);
              break;
          }
・・・

fusionJavaScriptHandlerプロパティ(なのかな?)で受け取る様です。
受け取ったdataを再度リストに分割し、再レンダーさせてます。

React・Babel共にCDNとしている事が原因なのか? そもそもReact・Babel
を利用する事が原因なのか? Fusion360で行うとこんなものなのか?
原因がハッキリしないのですが、最初の標示のレスポンスが異常に悪く、
最初の時点で無駄に何度かレンダーさせています。

    // 最初のレスポンスが悪すぎる
    root.render(<NamesList names={[" "]} />);
    root.render(<NamesList names={["  "]} />);
    root.render(<NamesList names={[]} />);

但し、完全な解決になっておらず、ボディを選択しても名前が表示されない
時があります。(特にダイアログが表示され直ぐに選択をした場合は失敗しやすい)


フロントサイドでゴリゴリやる予定も無く、単に動的な表示させたいだけの為に、
"React必要か?" とも感じますが、この路線で行きます。

曲線の長さと同期させる6

こちらの続きです。
曲線の長さと同期させる5 - C#ATIA

業務が忙しくなり、まとまった時間が確保出来ずほぼ進展無いのですが、
前回の動画で使用していたコードの一部を公開しておきます。
・・・紛失してしまいそうな予感がするからです。

import adsk.core
import adsk.fusion
import json


GROUPNAME = 'kantoku' # コマンド名にすべき
ATTRKEY = 'curves$'
_debug = True

class SynchronousParameterFactry():
    def __init__(self) -> None:
        self.containers = []

        self.registerContainers()

    def __getEntityByToken__(
        self,
        token: str):

        des: adsk.fusion.Design = self.__getActDoc__().design
        ents = des.findEntityByToken(token)
        if len(ents) < 1:
            return None

        return ents[0]


    def __getEntityByParameterAttr__(self, prm: adsk.fusion.UserParameter) -> list:
        attr: adsk.core.Attribute = prm.attributes.itemByName(GROUPNAME, ATTRKEY)
        if not attr:
            return

        crvsTokenDict: dict = json.loads(attr.value)
        lst = []
        for token in crvsTokenDict.values():
            crv = self.__getEntityByToken__(token)
            if not crv:
                continue

            lst.append(crv)

        return lst


    def __getHasAttrUserParameter__(self) -> list:
        prms: adsk.fusion.UserParameters = self.__getActDoc__().design.userParameters

        lst = []
        prm: adsk.fusion.UserParameter
        for prm in prms:
            if prm.attributes.itemByName(GROUPNAME, ATTRKEY):
                lst.append(prm)

        return lst


    def __getActDoc__(
        self,) -> adsk.fusion.FusionDocument:

        app: adsk.core.Application = adsk.core.Application.get()
        return app.activeDocument


    def __getContainer__(
        self,
        prm: adsk.fusion.UserParameter) -> 'ParameterContainer':

        for container in self.containers:
            if container.param != prm:
                continue
            return container
        
        return None


    def __initContainer__(
        self,
        parameter: adsk.fusion.UserParameter,
        crvs: list) -> None:

        container: 'ParameterContainer' = ParameterContainer(
            parameter,
            crvs
        )
        self.containers.append(container)

        return container


    def __updateContainer__(
        self,
        container: 'ParameterContainer',
        crvs: list) -> None:

        container.updateCurves(crvs)


    def registerContainers(self) -> None:
        self.containers = [] # これが必要か判断出来ない・・・
        prms = self.__getHasAttrUserParameter__()
        if len(prms) < 1:
            return

        prm: adsk.fusion.UserParameter
        for prm in prms:
            self.set(
                prm,
                self.__getEntityByParameterAttr__(prm)
            )


    def set(
        self,
        parameter: adsk.fusion.UserParameter,
        crvs: list) -> None:

        container: 'ParameterContainer' = self.__getContainer__(
            parameter,
        )

        if container:
            self.__updateContainer__(
                container,
                crvs
            )
        else:
            container = self.__initContainer__(
                parameter,
                crvs
            )

        container.updateParameter()


    def update(
        self) -> None:

        container: 'ParameterContainer'
        for container in self.containers:
            container.updateParameter()


    def getCurves(
        self,
        Parameter: adsk.fusion.UserParameter) -> list:

        container: 'ParameterContainer' = self.__getContainer__(
            Parameter,
        )

        if not container:
            return []

        return container.getCurves()

    def getParameterNames(
        self) -> list:

        return [container.param.name for container in self.containers]
            



# ************************************
class ParameterContainer():
    def __init__(
        self,
        parameter: adsk.fusion.UserParameter,
        curves: list) -> None:

        self.param: adsk.fusion.UserParameter = parameter
        self.curves: list = curves


    def __del__(self) -> None:
        self.param = None
        self.curves = []


    def __addAttr__(self) -> None:
        # curves tokens
        tokens = {str(idx): c.entityToken for idx, c in enumerate(self.curves)}

        # add attr
        attrs: adsk.core.Attributes = self.param.attributes
        attrs.add(
            GROUPNAME,
            ATTRKEY,
            json.dumps(tokens)
        )


    def __updateAttr__(self) -> None:
        # curves tokens
        tokens = {str(idx): c.entityToken for idx, c in enumerate(self.curves)}

        # add attr
        attrs: adsk.core.Attributes = self.param.attributes
        attr: adsk.core.Attribute = attrs.itemByName(
            GROUPNAME,
            ATTRKEY
        )

        if attr:
            if set(json.loads(attr.value).values()) == set(tokens.values()):
                return
            else:
                attr.value = json.dumps(tokens)
        else:
            self.__addAttr__()


    def __updatePrm__(self) -> None:
        if not self.param:
            return

        value = self.getCurvesLength()
        if self.param.value != value:
            self.param.value = value


    def __isVisible__(self, ent) -> bool:
        try:
            if not hasattr(ent, 'isVisible'):
                return False

            if ent.isVisible:
                return True
            return False
        except:
            return False


    def getCurvesLength(self) -> float:
        value = 0
        crv: adsk.fusion.SketchEntity
        for crv in self.curves:
            value += crv.length

        return value


    def updateParameter(self) -> None:
        # curves
        self.curves = [c for c in self.curves if self.__isVisible__(c)]

        # parameter
        self.__updatePrm__()

        # attr
        self.__updateAttr__()


    def updateCurves(self, curves: list) -> None:
        # curves
        newCrvs = [e for e in curves if self.__isVisible__(e)]
        oldCrvs = [e for e in self.curves if self.__isVisible__(e)]

        newToken = set(e.entityToken for e in newCrvs)
        oldToken = set(e.entityToken for e in oldCrvs)
        if newToken != oldToken:
            self.curves = newCrvs
        elif len(oldCrvs) != len(self.curves):
            self.curves = oldCrvs

        # parameter
        self.__updatePrm__()

        # attr
        self.__updateAttr__()


    def getCurves(self) -> list:
        return self.curves

念の為ですが、パラメータと監視すべき曲線を紐付けしておくための
クラスの為、これ単体では動きません。
これで完成では無く修正が必要なのですが、UIを作成しないと何が
必要なのか分からない状態に陥り、悶えています。

UIについてもどんな機能が必要なものか迷っている上、望んでいるような
ものを作る為にはかなり勉強する必要性を感じ、悩んでいます。
完成するかな? 捨てるにはもったいない気がしているのですが。