C#ATIA

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

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必要か?" とも感じますが、この路線で行きます。