C#ATIA

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

pythonからjsTreeへjson投げる2

こちらの続きです。
pythonからjsTreeへjson投げる1 - C#ATIA

こちら、思ったような動作をしてくれない事が分かり断念する事に・・・。
実はこちらに対応出来るアドインを作ろうとしていました。
Longer Script List, or Script Panel with Buttons - Autodesk Community

長ったらしい表示なので、Tree上に表示させて折りたたむことで
こじんまりさせようと思っていました。

python <-> jsTree(html) のやり取りは出来るようになりました。
スクリプトを実行する事は可能だったのですが、実行するとアドインも
終了してしまうため、ほぼメリットが無い状態となり断念する事にしました。
恐らくFusion360自体がそのような仕様なのでしょう。

汚いのですが、とりあえずテストしたものを残しておきます。
(無くしてしまいそうなので)

エントリーポイントとなるPalette_test.pyです。

# Fusion360API Python Addin
import adsk.core
import adsk.fusion
import traceback
import json
import math
# from .ktkLanguageMessage import LangMsg
from .ScriptsManager import ScriptsManager

# # Multilingual Dictionary
# msgDict = {
# }
# lm = LangMsg(msgDict, adsk.core.UserLanguages.JapaneseLanguage)


handlers = []
_app: adsk.core.Application = None
_ui: adsk.core.UserInterface = None

_cmdInfo = {
    'id': 'test',
    'name': 'test',
    'tooltip': 'test',
    'resources': ''
}

_paletteInfo = {
    'id': 'testPalette',
    'name': _cmdInfo["name"],
    'htmlFileURL': 'index.html',
    'isVisible': True,
    'showCloseButton': True,
    'isResizable': True,
    'width': 400,
    'height': 300,
    'useNewWebBrowser': False,  # True,#,
    'dockingState': None
}

class MyHTMLEventHandler(adsk.core.HTMLEventHandler):
    def __init__(self):
        super().__init__()
        self.treeJson = None
        self.scriptsMgr = None

    def notify(self, args):
        try:
            htmlArgs = adsk.core.HTMLEventArgs.cast(args)

            global _app
            if htmlArgs.action == 'htmlLoaded':

                # インポートするフォルダーパス
                path = r'C:\temp'

                self.scriptsMgr = ScriptsManager()
                self.scriptsMgr.addInsideFolder(path)
                jstreeJson = self.scriptsMgr.getJstreeJson()

                args.returnData = json.dumps({
                    'action': 'send',
                    'data' : jstreeJson,
                })

            elif htmlArgs.action == 'execScript':
                data = json.loads(htmlArgs.data)
                script = self.scriptsMgr.getItemById(int(data['id']))
                if script:
                    script.exec()

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


def initPallet():
    global _ui, _paletteInfo

    palette = _ui.palettes.itemById(_paletteInfo['id'])
    if palette:
        palette.deleteMe()

    palette = _ui.palettes.add(
        _paletteInfo['id'],
        _paletteInfo['name'],
        _paletteInfo['htmlFileURL'],
        _paletteInfo['isVisible'],
        _paletteInfo['showCloseButton'],
        _paletteInfo['isResizable'],
        _paletteInfo['width'],
        _paletteInfo['height'],
        _paletteInfo['useNewWebBrowser'],
    )

    if _paletteInfo['dockingState']:
        palette.dockingState = _paletteInfo['dockingState']
    else:
        palette.setPosition(800, 400)

    onHTMLEvent = MyHTMLEventHandler()
    palette.incomingFromHTML.add(onHTMLEvent)
    handlers.append(onHTMLEvent)

    onClosed = MyCloseEventHandler()
    palette.closed.add(onClosed)
    handlers.append(onClosed)

class ShowPaletteCommandExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args):
        try:
            initPallet()
        except:
            _ui.messageBox('Command executed failed: {}'.format(
                traceback.format_exc()))


class MyCloseEventHandler(adsk.core.UserInterfaceGeneralEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args):
        try:
            pass
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


class ShowPaletteCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args):
        try:
            command = args.command
            onExecute = ShowPaletteCommandExecuteHandler()
            command.execute.add(onExecute)
            handlers.append(onExecute)
        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


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

        global _cmdInfo
        showPaletteCmdDef = _ui.commandDefinitions.itemById(_cmdInfo['id'])
        if showPaletteCmdDef:
            showPaletteCmdDef.deleteMe()

        showPaletteCmdDef = _ui.commandDefinitions.addButtonDefinition(
            _cmdInfo['id'],
            _cmdInfo['name'],
            _cmdInfo['tooltip'],
            _cmdInfo['resources']
        )

        global handlers
        onCommandCreated = ShowPaletteCommandCreatedHandler()
        showPaletteCmdDef.commandCreated.add(onCommandCreated)
        handlers.append(onCommandCreated)

        panel = _ui.allToolbarPanels.itemById('SolidScriptsAddinsPanel')
        cntrl = panel.controls.itemById('showPalette')
        if not cntrl:
            panel.controls.addCommand(showPaletteCmdDef)
    except:
        if _ui:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


def stop(context):
    try:
        global _paletteInfo
        palette = _ui.palettes.itemById(_paletteInfo['id'])
        if palette:
            palette.deleteMe()

        global _cmdInfo
        panel = _ui.allToolbarPanels.itemById('SolidScriptsAddinsPanel')
        cmd = panel.controls.itemById(_cmdInfo['id'])
        if cmd:
            cmd.deleteMe()
        cmdDef = _ui.commandDefinitions.itemById(_cmdInfo['id'])
        if cmdDef:
            cmdDef.deleteMe()

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

続いて、Tree表示させたるスクリプトのパスや情報、起動を担当する
コンテナーScriptsManager.pyです。

# Fusion360API Python Addin

import traceback
import adsk.fusion
import adsk.core
import sys
import pathlib
import json
import re
from typing import List
from dataclasses import dataclass
from dataclasses import field
from importlib import import_module

_CONTEXT = {'IsApplicationStartup': True}

@dataclass
class ScriptContainer:
    id: int
    path: pathlib.WindowsPath

    def __post_init__(self):
        pass

    def toJson(self):
        stem = self.path.stem
        # ここでアイコン読み込みたい
        return {
            'id' : self.id,
            'text' : stem,
            # 'icon' : None,
        }

    def exec(self):
        app = None
        def _getModule(path):
            try:
                stem = path.stem

                sys.path.append(str(path))
                module = import_module(stem)
            except:
                module = None
            finally:
                del sys.path[-1]

            return module

        # *****
        try:
            app: adsk.core.Application = adsk.core.Application.get()

            # モジュール取得
            script = _getModule(self.path)

            # 外部スプリクト実行
            global _CONTEXT

            script.run(_CONTEXT)

        except:
            app.log('Failed:\n{}'.format(traceback.format_exc()))

@dataclass
class ScriptsManager:
    count: int = field(default=1, compare=False)
    # patterns: List[str] = field(default_factory=list)
    items: List[pathlib.WindowsPath] = field(default_factory=list)

    def add(
        self,
        path: str) -> bool:

        pathObj: pathlib.WindowsPath = pathlib.Path(path)
        if not self.isFusionScript(pathObj):
            return False

        item: dataclass = ScriptContainer(
            self.count,
            pathObj
        )
        self.count += 1
        self.items.append(item)

        return True

    def addInsideFolder(
        self,
        path: str) -> bool:

        pathlib_obj: pathlib.WindowsPath = pathlib.Path(path)
        files = list(pathlib_obj.glob("*"))

        for file in files:
            if file.is_dir() and self.isFusionScript(file):
                self.add(file)

        return True

    def isFusionScript(
        self,
        pathObj: pathlib.Path) -> bool:

        # check filesS
        stem = pathObj.stem
        requiredList = [
            f'{stem}.manifest',
            f'{stem}.py'
        ]

        files = [f.name for f in  list(pathObj.glob("*"))]
        if len(set(requiredList) & set(files)) != len(requiredList):
            return False

        # check manifest
        manifest = pathObj / requiredList[0]
        with open(str(manifest),'r', encoding='utf-8') as f:
            data = f.read()

        data_json = json.loads(data) 

        if not 'type' in data_json:
            return False

        if data_json['type'] != 'script':
            return False

        return True

    def getItemById(
        self,
        id: int) -> dataclass:

        for item in self.items:
            if item.id == id:
                return item

        return None

    def groupBy(
        self) -> list:

        # sort
        items = sorted(self.items, key = lambda x:x.path.stem.lower())

        # key
        # コンストラクタにキーを入れるべき
        patterns = [
            # 'a1',
            ['A-E', '^[a-e]'],
            ['F-J', '^[f-j]'],
            ['K-O', '^[k-o]'],
            ['P-T', '^[p-t]'],
            ['U-Z', '^[u-z]'],
        ]
        otherKey = ['other', 'other']

        group = {key[0]: [] for key in patterns}
        group[otherKey[0]] = []

        for item in items:
            stem = item.path.stem
            hitFg = False
            for pattern in patterns:
                match = re.search(pattern[1], stem.lower())
                if match:
                    group[pattern[0]].append(item)
                    hitFg = True
                    break
            if not hitFg:
                group[otherKey[0]].append(item)

        return group

    def getJstreeJson(
        self):

        # https://www.jstree.com/
        groups = self.groupBy()
        treeContent = []
        groupCount = self.count
        for key in groups:
            if len(groups[key]) < 1:
                continue

            children = [item.toJson() for item in groups[key]]
            treeContent.append(
                {
                    'id' : groupCount,
                    'text' : key,
                    'children' : children,
                }
            )

            groupCount += 1

        return treeContent

最後にTreeを表示させるパレットのindex.htmlです。

!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>jstree basic demos</title>
    <style>
      html {
        margin: 0;
        padding: 0;
        font-size: 62.5%;
      }
      body {
        max-width: 800px;
        min-width: 300px;
        margin: 0 auto;
        padding: 20px 10px;
        font-size: 14px;
        font-size: 1.4em;
      }
      .tree {
        overflow: auto;
        border: 1px solid silver;
        min-height: 100px;
      }
    </style>
    <link rel="stylesheet" href="./dist/themes/default/style.min.css" />
  </head>
  <body>
    <button id="evts_button">test</button>
    <em>test</em>
    <div id="tree" class="tree"></div>
    <p id="event_result">-</p>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="./dist/jstree.min.js"></script>

    <script>
      // document load
      window.onload = function () {
        var adskWaiter = setInterval(function () {
          if (window.adsk) {
            clearInterval(adskWaiter);

            let ret = adsk.fusionSendData("htmlLoaded", "");
            let obj = JSON.parse(ret);
            $("#tree").jstree(true).settings.core.data = obj.data;
            $("#tree").jstree(true).refresh();
          }
        }, 100);
      };

      // init tree
      $("#tree").jstree({
        core: {
          multiple: false,
          data: [
            {
              text: "Loading...",
            },
          ],
        },
      });

      // double click
      $('#tree').on('dblclick', '.jstree-anchor', function(e){
        var args = {
          id: $(this).parent().attr('id')
        }
        adsk.fusionSendData("execScript", JSON.stringify(args))
      });

    </script>
  </body>
</html>

折角表示させる事が出来たから、Treeを何かに使いたいな。