master 4b07662a38fe cached
34 files
86.7 KB
22.5k tokens
89 symbols
1 requests
Download .txt
Repository: xzhuah/BlenderAddonPackageTool
Branch: master
Commit: 4b07662a38fe
Files: 34
Total size: 86.7 KB

Directory structure:
gitextract_qx3brcgf/

├── .gitignore
├── LICENSE
├── README.md
├── addons/
│   ├── __init__.py
│   └── sample_addon/
│       ├── __init__.py
│       ├── blender_manifest.toml
│       ├── config.py
│       ├── i18n/
│       │   ├── __init__.py
│       │   └── dictionary.py
│       ├── operators/
│       │   ├── AddonOperators.py
│       │   └── __init__.py
│       ├── panels/
│       │   ├── AddonPanels.py
│       │   └── __init__.py
│       └── preference/
│           ├── AddonPreferences.py
│           └── __init__.py
├── common/
│   ├── __init__.py
│   ├── class_loader/
│   │   ├── __init__.py
│   │   ├── auto_load.py
│   │   └── module_installer.py
│   ├── i18n/
│   │   ├── __init__.py
│   │   ├── dictionary.py
│   │   └── i18n.py
│   ├── io/
│   │   ├── FileManagerClient.py
│   │   └── __init__.py
│   └── types/
│       ├── __init__.py
│       └── framework.py
├── create.py
├── framework.py
├── main.py
├── release.py
├── requirements.txt
├── test.py
├── wheels/
│   └── README.md
└── workflows/
    └── addon_release.yml

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/#use-with-ide
.pdm.toml

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
config.ini

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 ZHU Xinyu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Blender Add-on Development Framework and Packaging Tool

### Demo 1: Auto-update while developing 开发过程支持自动更新

![Demo1](https://github.com/xzhuah/demo_resource/blob/main/blender_addon_tool_demo1.gif)

### Demo 2: Built-in I18n solution 内置字典翻译方案

![Demo2](https://github.com/xzhuah/demo_resource/blob/main/blender_addon_tool_demo2.gif)

### Demo 3: Load Blender Component class like Operation, Panel etc automatically 自动加载Blender组件类(Operation, Panel等)

![Demo2](https://github.com/xzhuah/demo_resource/blob/main/blender_addon_tool_demo3.gif)

The project has been updated to use MIT open source license since 2024.12.02.

This project provides a lightweight, easy-to-use framework for developing and packaging Blender addons. The main
features include:

1. A single command to create a template add-on, facilitating quick development.
1. Support developing multiple addons in one project, which allows you to reuse functions across
   different addons, and helps you package only necessary modules into the zip file.
1. Allows you to run and test your add-on in Blender with a single command. Support hot swap
   for code updates. You can see the updates in Blender immediately after saving the code.
1. A single command to package the add-on into an installable addon package, making it easier for users to install. The
   packaging tool automatically detects and includes dependencies needed for the add-on within your project. (Not
   including 3rd party libraries)
1. Provide utility functions for basic development, like an auto-load utility for automatic class loading and an
   internationalization (i18n)
   tool, to help new developers creating high-quality addons.
1. Support extension development in Blender 4.2 and later versions. You can choose to package your addon
   as a legacy addon or as an extension by setting the IS_EXTENSION configuration

You can check out an overview about this framework on YouTube:

- https://www.youtube.com/watch?v=eRSXO_WkY0s
- https://youtu.be/udPBrXJZT1g

The following external library will be installed automatically when you run the framework code, you can also install
them
manually:

- https://github.com/nutti/fake-bpy-module
- https://github.com/gorakhargosh/watchdog

## Basic Framework

- [main.py](main.py): Configures the Blender path, add-on installation path, default add-on, package ignore files, and
  add-on release path, among other settings.
- [test.py](test.py): A testing tool to run and test add-ons.
- [create.py](create.py): A tool to create add-ons, allowing you to quickly create an add-on based on the `sample_addon`
  template.
- [release.py](release.py): A packaging tool that packages add-ons into an installable package.
- [framework.py](framework.py): The core business logic of the framework, which automates the development process.
- [addons](addons): A directory to store add-ons, with each add-on in its own sub-directory. Use `create.py` to quickly
  create a new add-on.
- [common](common): A directory to store shared utilities.

## Framework Development Guidelines

Blender Version >= 2.93
Platform Supported: Windows, MacOs, Linux

Each add-on, while adhering to the basic structure of a Blender add-on, should include a `config.py` file to configure
the add-on's package name, ensuring it doesn't conflict with other add-ons.

This project depends on the `addons` folder; do not rename this folder.

When packaging an add-on, the framework will generate a __init__.py file in the add-on directory. By copying bl_info,
and importing the register and unregister method from your target addon's __init__.py. Usually this won't cause any
issue, but if you notice anything that might be related to this, please let us know.

### Notice for extension developers

To meet the standard
at https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html#user-preferences-and-package
You need to follow the following instruction when using preferences in your extension addon. These instructions are also
applicable to the legacy addon, but not enforced.

Since addons developed by this framework usually have submodules. To access preferences, you must use the
__addon_name__ defined in the config.py file as the bl_idname of the preferences.

Define:

```python
class ExampleAddonPreferences(AddonPreferences):
    bl_idname = __addon_name__
```

Access

```python
from ..config import __addon_name__

addon_prefs = bpy.context.preferences.addons[__addon_name__].preferences
# use addon_prefs
addon_prefs.some_property
```

## Usage

1. Clone this repository.
1. Open this project in your IDE. Optional: Configure the IDE to use the same python interpreter as Blender.
1. Note: For PyCharm users, change the value idea.max.intellisense.filesize in idea.properties file ( Help | Edit Custom
   Properties.) to more than 2600 because some modules have the issue of being too big for intelliSense to work. You
   might also need to associate the __init__.pyi file as the python File Types
   in ![setting](https://i.ibb.co/QcYZytw/script.png) to get the auto code completion working.
1. Configure the name of the addon you want to create (ACTIVE_ADDON) in [main.py](main.py).
1. Run create.py to create a new addon in your IDE. The first time you run this, it will download dependencies,
   including
   watchdog and fake-bpy-module.
1. Develop your addon in the newly created addon directory.
1. Run test.py to test your addon in Blender.
1. Run release.py to package your addon into an installable package. The packaged addon path will appears in the
   terminal when packaged successfully.

## Features Provided by the Framework

1. You don't need to worry about register and unregister classes in Blender add-ons. The framework automatically loads
   and unloads classes in your add-ons. You just need to define your classes in the addon's folder. Note that the
   classes that are automatically loaded need to be placed in a directory with an `__init__.py` file to be recognized
   and loaded by the framework's auto load mechanism.
1. You can use internationalization in your add-ons. Just add translations in the standard format to the `dictionary.py`
   file in the `i18n` folder of your add-on.
1. You can define RNA properties declaratively. Just follow the examples in the `__init__.py` file to add your RNA
   properties. The framework will automatically register and unregister your RNA properties.
1. You can choose to package your addon as a legacy addon or as an extension in Blender 4.2 and later versions. Just set
   the `IS_EXTENSION` configuration to switch between the two. The framework will convert absolute import to relative
   import for you when releasing.
   Notice only `from XXX.XXX import XXX` is supported, `import XXX.XX` is not supported for converting to relative
   import.
1. You can use the `ExpandableUi` class in `common/types/framework.py` to easily extend Blender's native UI components,
   such as menus, panels, pie menus, and headers. Just inherit from this class and implement the `draw` method. You can
   specify the ID of the native UI component you want to extend using `target_id` and specify whether to append or
   prepend using `expand_mode`.
1. You can use the `reg_order` decorator in `common/types/framework.py` to specify the order of registration for your
   classes. This is useful when you need to ensure that certain classes are registered before others. For example the
   initial order of Panels will be in the order they are registered.

## Add Optional Configuration File

To avoid having to modify the configuration items in `main.py` every time you update the framework, you can create a
`config.ini` file in the root directory of your project to store your configuration information. This file will override
the configuration information in `main.py`.

Here is an example of a `config.ini` file:

```ini
[blender]
; path to the blender executable
exe_path = C:/software/general/Blender/Blender3.5/blender.exe
; exe_path = C:/software/general/Blender/Blender3.6/blender.exe

; path to the addon directory, testing addon will be temporarily installed here
; usually you don't need to configure this since it can be derived from the exe_path
addon_path = C:/software/general/Blender/Blender3.5/scripts/addons/

[default]
; name of the addon to be created, tested and released
addon = sample_addon
; Whether the addon is an extension, if True, the addon will be packaged when released.
is_extension = False
; the path to store released addon zip files. Do not release to your source code directory
release_dir = C:/path/to/release/dir
; the path to store addon files used for testing, during testing, the framework will first release the addon to here and copy it to Blender's addon directory. Do not release to your source code directory
test_release_dir = C:/path/to/test/release/dir
```

## Contributions

1. Framework Updates: If you are using this framework in your project and need to migrate to a newer version, you will
   need to manually replace the framework files to get the new features. You may fork this project and use
   `git fetch upstream` to update.
   We are looking for more user-friendly migration
   experience. In general, we aim to keep the framework lightweight and avoid making structural changes. Most future
   updates are expected to just adding new features rather than making major changes to the framework structure. So
   unless you personally made changes to the framework code locally, you will only need to replace the old files with
   the new ones in future updates.
1. Breakpoint Debugging: The framework currently does not support breakpoint debugging within the IDE. Implementing this
   feature requires some modifications to the framework code, which may increase the complexity of using the framework.
   We are looking for a lightweight solution to enable this feature. However, in general, breakpoint debugging is not
   necessary for developing add-ons. Breakpoint debugging is helpful for complex add-ons features, but logging is
   sufficient in most of the cases. For this framework, breakpoint debugging would be a nice-to-have feature, but not a
   must-have.

# Blender 插件开发框架及打包工具

本项目已于2024.12.02更新为MIT开源许可证。

本项目是一个轻量级的Blender插件开发框架和打包工具. 主要功能包括:

1. 一条命令创建模版插件,方便进行快速开发
1. 支持在一个项目中开发多个插件,可以让你在不同的插件之间复用函数功能,它可以自动检测功能模块之间的依赖关系,将相关联的模块打包到zip文件中,而不包含不必要的模块
1. 在IDE中可以通过一条命令在Blender上运行插件的测试, 支持代码热更新,保存代码后可以立即在Blender中看到变化
1. 一条命令将插件打包成一个安装包,方便用户安装,打包工具自动检测插件的依赖关系,自动打包插件所需的依赖文件(
   不包括引用的外部库)
1. 提供了常用的插件开发工具,比如自动加载类的auto_load工具,提供国际化翻译的i18n工具,方便新手开发者进行高水平插件开发
1. 你可以选择将你的插件打包成传统插件或者扩展插件,只需要设置IS_EXTENSION配置即可切换,框架会在打包时自动将绝对导入转换为相对导入
   注意只支持`from XXX.XXX import XXX`这种形式的转换,`import XXX.XX`这种形式的导入不支持转换为相对导入
1. 兼容Blender 4.2及以后版本的扩展开发,你可以选择将你的插件打包成传统插件或者扩展插件,只需要设置IS_EXTENSION配置即可切换

欢迎观看我们的中文视频教程:

- [B站](https://www.bilibili.com/video/BV1Gn4y1d7Bp)
- [深度技术讲解](https://www.bilibili.com/video/BV1VBqcY4E6x)
- [YouTube](https://www.youtube.com/watch?v=Pjf7wg3IzDE&list=PLPkz3Ny42tJtxzw7xVUWvLI3FwEeETVOj&index=1&t=5s)

下外部库会在框架代码运行时自动安装,你也可以手动安装它们:

- https://github.com/nutti/fake-bpy-module
- https://github.com/gorakhargosh/watchdog

## 基础框架

[main.py](main.py): 可以配置Blender路径,插件安装路径,当前默认插件,插件发布路径等

[test.py](test.py): 测试工具,可以运行插件的测试

[create.py](create.py): 创建插件的工具,可以根据sample_addon模版快速创建一个插件

[release.py](release.py): 打包工具,可以将插件打包成一个安装包

[framework.py](framework.py): 框架的核心业务代码,用于实现开发流程的自动化

[addons](addons): 存放插件的目录,每个插件一个目录,使用create.py可以快速创建一个插件

[common](common): 存放公共工具的目录

## 框架开发要求

Blender 版本 >= 2.93
支持的平台: Windows, MacOs, Linux

每个插件在符合Blender插件的结构基础上,需要有一个config.py文件用于配置插件的包名,避免与其他插件冲突。

注意项目依赖addons文件夹,请勿更改这个文件夹的名称。

在打包插件时,框架会在插件目录下生成一个__init__.py文件,这个__init__.py文件会复制你的插件的__init__.py文件中bl_info,
并导入register和unregister方法。
通常这不会引起任何问题,但如果你发现与这个有关的问题,请与我们联系。

### 扩展插件开发注意事项

为了满足https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html#user-preferences-and-package
的要求,当你在扩展插件中定义和使用偏好设置时,你需要遵循以下要求。这些要求也适用于传统插件,但不是强制的。

由于本框架开发的插件通常带有子模块,为了定义和访问插件的偏好设置,你必须使用config.py文件中定义的__addon_name__作为偏好设置的bl_idname。

定义偏好设置类:

```python
class ExampleAddonPreferences(AddonPreferences):
    bl_idname = __addon_name__
```

访问偏好设置:

```python
from ..config import __addon_name__

addon_prefs = bpy.context.preferences.addons[__addon_name__].preferences
# use addon_prefs
addon_prefs.some_property
```

## 使用说明

1. 克隆此项目。
1. 在您的 IDE 中打开此项目。你可以将IDE使用的Python.exe配置成与Blender相同。
1. 对于PyCharm用户,请将idea.properties文件(点击 Help | Edit Custom Properties.)
   中的idea.max.intellisense.filesize的值更改为大于2600,因为某些模块的大小超过了intelliSense的工作范围。你可能需要将__init__
   .pyi文件关联到python File Types ![setting](https://i.ibb.co/QcYZytw/script.png) 以使自动代码补全正常工作。
1. 在 [main.py](main.py) 中配置 Blender 可执行文件路径(BLENDER_EXE_PATH)
1. 在 [main.py](main.py) 中配置您想要创建的插件名称(ACTIVE_ADDON)。
1. 运行 create.py 在您的 IDE 中创建一个新的插件。第一次运行时需要联网下载依赖库,包括watchdog和fake-bpy-module
1. 在新创建的插件目录中开发您的插件。
1. 运行 test.py 在 Blender 中测试您的插件。
1. 运行 release.py 将您的插件打包成可安装的包。成功打包后,终端中将显示打包插件的路径。

## 框架提供的功能

1.
你基本上无需关心Blender插件的类的加载和卸载,框架会自动加载和卸载你的插件中的类,你只需要在插件目录下定义你的类即可,注意自动加载的类需要放在有__init__
.py文件的目录下才能被框架自动类加载机制识别并加载
1. 你可以在插件中使用国际化翻译,只需要在插件文件夹中的i18n中的dictionary.py文件中按标准格式添加翻译即可
1. 你可以使用声明式的方式定义RNA属性,只需要根据__init__.py中的注释示例添加你的RNA属性即可,框架会自动注册和卸载你的RNA属性
1. 你可以使用common/types/framework.py中的ExpandableUi类来方便的扩展Blender原生的菜单,面板,饼菜单,标题栏等UI组件,
   只需继承该类并实现draw方法,你可以通过target_id来指定需要扩展的原生UI组件的ID,
   通过expand_mode来指定向前还是向后扩展。
1. 你可以使用common/types/framework.py中的reg_order装饰器来指定类的注册顺序,当你需要确保某些类在其他类之前注册时,可以利用这个功能。
   比如Panel的初始顺序将会按照注册的顺序来排列。

## 添加可选的配置文件

为了避免每次更新框架时需要重新修改main.py中的配置,你可以在项目的根目录下创建一个config.ini文件,用于存放你的配置信息,
这个文件会覆盖main.py中的配置信息。

以下是一个config.ini文件的示例:

```ini
[blender]
; Blender的可执行文件路径
exe_path = C:/software/general/Blender/Blender3.5/blender.exe
; exe_path = C:/software/general/Blender/Blender3.6/blender.exe

; 插件目录路径,测试时插件将被临时安装到这里
; 通常不需要配置此项,因为框架可以通过exe_path的路径推导出来
addon_path = C:/software/general/Blender/Blender3.5/scripts/addons/
[default]
; 创建、测试和发布的目标插件名称
addon = sample_addon
; 插件是否为扩展,如果为True,则插件在发布时会被打包成扩展的形式
is_extension = False
; 发布插件zip文件的存放路径。注意不要发布到源码所在的目录中
release_dir = C:/path/to/release/dir
; 用于测试时插件文件的临时存放路径,测试是框架首先会发布插件到这里,然后再复制到Blender的插件目录。注意不要发布到源码所在的目录中
test_release_dir = C:/path/to/test/release/dir
```

## 框架在以下方面可进一步完善,欢迎贡献意见和代码

1. 框架的更新:如果你已经在项目中使用了这个框架进行开发,当你需要迁移到更新的框架版本时,你需要手动替换框架代码文件来获取新的功能,你可以fork这个项目,然后使用
   `git fetch upstream`来拉取最新的代码。
   我们希望探索一些更友好的方式来帮助更新框架代码,欢迎提供意见。但总的来说,我们希望框架保持轻量,不会在结构上有太大的变化。
   可以预计未来的大部分更新都是在增加新的功能,而不是对框架结构进行大的调整。除非你在本地对框架代码有所改动,未来的更新你只需要将新的文件替换旧的文件即可。
1. 断点调试:目前框架并不支持IDE内的断点调试,实现这个功能需要对框架代码进行一些修改,这也许会增加框架的使用难度,我们力求寻找尽量轻量级的解决方案。但总的来说,
   断点调试对应开发插件并不是必须的,大部分的插件功能并没有复杂到需要断点调试,打印日志也可以达到大部分的调试的目的。对于这个框架,如果我们力求寻找一个简单的方案来支持断点调试,但这不是必须的。


================================================
FILE: addons/__init__.py
================================================


================================================
FILE: addons/sample_addon/__init__.py
================================================
import bpy

from .config import __addon_name__
from .i18n.dictionary import dictionary
from ...common.class_loader import auto_load
from ...common.class_loader.auto_load import add_properties, remove_properties
from ...common.i18n.dictionary import common_dictionary
from ...common.i18n.i18n import load_dictionary

# Add-on info
bl_info = {
    "name": "Basic Add-on Sample",
    "author": "[You name]",
    "blender": (3, 5, 0),
    "version": (0, 0, 1),
    "description": "This is a template for building addons",
    "warning": "",
    "doc_url": "[documentation url]",
    "tracker_url": "[contact email]",
    "support": "COMMUNITY",
    "category": "3D View"
}

_addon_properties = {}


# You may declare properties like following, framework will automatically add and remove them.
# Do not define your own property group class in the __init__.py file. Define it in a separate file and import it here.
# 注意不要在__init__.py文件中自定义PropertyGroup类。请在单独的文件中定义它们并在此处导入。
# _addon_properties = {
#     bpy.types.Scene: {
#         "property_name": bpy.props.StringProperty(name="property_name"),
#     },
# }

def register():
    # Register classes
    auto_load.init()
    auto_load.register()
    add_properties(_addon_properties)

    # Internationalization
    load_dictionary(dictionary)
    bpy.app.translations.register(__addon_name__, common_dictionary)

    print("{} addon is installed.".format(__addon_name__))


def unregister():
    # Internationalization
    bpy.app.translations.unregister(__addon_name__)
    # unRegister classes
    auto_load.unregister()
    remove_properties(_addon_properties)
    print("{} addon is uninstalled.".format(__addon_name__))


================================================
FILE: addons/sample_addon/blender_manifest.toml
================================================
schema_version = "1.0.0"

# Example of manifest file for a Blender extension
# Change the values according to your extension
# id must be the same as the directory name, and must be unique
id = "sample_addon"
version = "0.0.1"
name = "Basic Add-on Sample"
tagline = "This is another extension"
maintainer = "Developer name <email@address.com>"
# Supported types: "add-on", "theme"
type = "add-on"

# Optional link to documentation, support, source files, etc
# website = "https://extensions.blender.org/add-ons/my-example-package/"

# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
tags = ["Animation", "Sequencer"]

blender_version_min = "4.2.0"
# # Optional: Blender version that the extension does not support, earlier versions are supported.
# # This can be omitted and defined later on the extensions platform if an issue is found.
# blender_version_max = "5.1.0"

# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
license = [
    "SPDX:GPL-3.0-or-later",
]
# Optional: required by some licenses.
# copyright = [
#   "2002-2024 Developer Name",
#   "1998 Company Name",
# ]

# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
# Other supported platforms: "windows-arm64", "macos-x64"

# Optional: bundle 3rd party Python modules.
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
# wheels = [
#   "./wheels/hexdump-3.3-py3-none-any.whl",
#   "./wheels/jsmin-3.0.1-py3-none-any.whl",
# ]

# # Optional: add-ons can list which resources they will require:
# # * files (for access of any filesystem operations)
# # * network (for internet access)
# # * clipboard (to read and/or write the system clipboard)
# # * camera (to capture photos and videos)
# # * microphone (to capture audio)
# #
# # If using network, remember to also check `bpy.app.online_access`
# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
# #
# # For each permission it is important to also specify the reason why it is required.
# # Keep this a single short sentence without a period (.) at the end.
# # For longer explanations use the documentation or detail page.
#
# [permissions]
# network = "Need to sync motion-capture data to server"
# files = "Import/export FBX from/to disk"
# clipboard = "Copy and paste bone transforms"

# Optional: build settings.
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
# [build]
# paths_exclude_pattern = [
#   "__pycache__/",
#   "/.git/",
#   "/*.zip",
# ]

================================================
FILE: addons/sample_addon/config.py
================================================
from ...common.types.framework import is_extension

# https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html#extensions-and-namespace
# This is the unique package name of the addon, it is different from the add-on name in bl_info.
# This name is used to identify the add-on in python code. It should also be the same to the package name of the add-on.
__addon_name__ = ".".join(__package__.split(".")[0:3]) if is_extension() else __package__.split(".")[0]


================================================
FILE: addons/sample_addon/i18n/__init__.py
================================================


================================================
FILE: addons/sample_addon/i18n/dictionary.py
================================================
from common.i18n.dictionary import preprocess_dictionary

dictionary = {
    "zh_CN": {
        ("*", "Example Addon Side Bar Panel"): "示例插件面板",
        ("*", "Example Functions"): "示例功能",
        ("*", "ExampleAddon"): "示例插件",
        ("*", "Resource Folder"): "资源文件夹",
        ("*", "Int Config"): "整数参数",
        # This is not a standard way to define a translation, but it is still supported with preprocess_dictionary.
        "Boolean Config": "布尔参数",
        "Second Panel": "第二面板",
        ("*", "Add-on Preferences View"): "插件设置面板",
        ("Operator", "ExampleOperator"): "示例操作",
    }
}

dictionary = preprocess_dictionary(dictionary)

dictionary["zh_HANS"] = dictionary["zh_CN"]


================================================
FILE: addons/sample_addon/operators/AddonOperators.py
================================================
import bpy

from ..config import __addon_name__
from ..preference.AddonPreferences import ExampleAddonPreferences


# This Example Operator will scale up the selected object
class ExampleOperator(bpy.types.Operator):
    '''ExampleAddon'''
    bl_idname = "object.example_ops"
    bl_label = "ExampleOperator"

    # 确保在操作之前备份数据,用户撤销操作时可以恢复
    bl_options = {'REGISTER', 'UNDO'}

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return context.active_object is not None

    def execute(self, context: bpy.types.Context):
        addon_prefs = bpy.context.preferences.addons[__addon_name__].preferences
        assert isinstance(addon_prefs, ExampleAddonPreferences)
        # use operator
        # bpy.ops.transform.resize(value=(2, 2, 2))

        # manipulate the scale directly
        context.active_object.scale *= addon_prefs.number
        return {'FINISHED'}


================================================
FILE: addons/sample_addon/operators/__init__.py
================================================


================================================
FILE: addons/sample_addon/panels/AddonPanels.py
================================================
import bpy

from ..config import __addon_name__
from ..operators.AddonOperators import ExampleOperator
from ....common.i18n.i18n import i18n
from ....common.types.framework import reg_order


class BasePanel(object):
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "ExampleAddon"

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return True


@reg_order(0)
class ExampleAddonPanel(BasePanel, bpy.types.Panel):
    bl_label = "Example Addon Side Bar Panel"
    bl_idname = "SCENE_PT_sample"

    def draw(self, context: bpy.types.Context):
        addon_prefs = context.preferences.addons[__addon_name__].preferences

        layout = self.layout

        layout.label(text=i18n("Example Functions") + ": " + str(addon_prefs.number))
        layout.prop(addon_prefs, "filepath")
        layout.separator()

        row = layout.row()
        row.prop(addon_prefs, "number")
        row.prop(addon_prefs, "boolean")

        layout.operator(ExampleOperator.bl_idname)

    @classmethod
    def poll(cls, context: bpy.types.Context):
        return True


# This panel will be drawn after ExampleAddonPanel since it has a higher order value
@reg_order(1)
class ExampleAddonPanel2(BasePanel, bpy.types.Panel):
    bl_label = "Example Addon Side Bar Panel"
    bl_idname = "SCENE_PT_sample2"

    def draw(self, context: bpy.types.Context):
        layout = self.layout
        layout.label(text="Second Panel")
        layout.operator(ExampleOperator.bl_idname)


================================================
FILE: addons/sample_addon/panels/__init__.py
================================================


================================================
FILE: addons/sample_addon/preference/AddonPreferences.py
================================================
import os

import bpy
from bpy.props import StringProperty, IntProperty, BoolProperty
from bpy.types import AddonPreferences

from ..config import __addon_name__


class ExampleAddonPreferences(AddonPreferences):
    # this must match the add-on name (the folder name of the unzipped file)
    bl_idname = __addon_name__

    # https://docs.blender.org/api/current/bpy.props.html
    # The name can't be dynamically translated during blender programming running as they are defined
    # when the class is registered, i.e. we need to restart blender for the property name to be correctly translated.
    filepath: StringProperty(
        name="Resource Folder",
        default=os.path.join(os.path.expanduser("~"), "Documents", __addon_name__),
        subtype='DIR_PATH',
    )
    number: IntProperty(
        name="Int Config",
        default=2,
    )
    boolean: BoolProperty(
        name="Boolean Config",
        default=False,
    )

    def draw(self, context: bpy.types.Context):
        layout = self.layout
        layout.label(text="Add-on Preferences View")
        layout.prop(self, "filepath")
        layout.prop(self, "number")
        layout.prop(self, "boolean")


================================================
FILE: addons/sample_addon/preference/__init__.py
================================================


================================================
FILE: common/__init__.py
================================================


================================================
FILE: common/class_loader/__init__.py
================================================


================================================
FILE: common/class_loader/auto_load.py
================================================
import importlib
import inspect
import pkgutil
import typing
from pathlib import Path

import bpy

__all__ = (
    "init",
    "register",
    "unregister",
    "add_properties",
    "remove_properties",
)

from ..types.framework import ExpandableUi, is_extension

blender_version = bpy.app.version

modules = None
ordered_classes = None
frame_work_classes = None


def init():
    global modules
    global ordered_classes
    global frame_work_classes
    # notice here, the path root is the root of the project
    modules = get_all_submodules(Path(__file__).parent.parent.parent)
    ordered_classes = get_ordered_classes_to_register(modules)
    frame_work_classes = get_framework_classes(modules)


def register():
    for cls in ordered_classes:
        bpy.utils.register_class(cls)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "register"):
            module.register()

    for cls in frame_work_classes:
        register_framework_class(cls)

def unregister():
    for cls in reversed(ordered_classes):
        bpy.utils.unregister_class(cls)

    for module in modules:
        if module.__name__ == __name__:
            continue
        if hasattr(module, "unregister"):
            module.unregister()

    for cls in frame_work_classes:
        unregister_framework_class(cls)


# Import modules
#################################################

def get_all_submodules(directory):
    return list(iter_submodules(directory))


def iter_submodules(path):
    import_as_extension = is_extension()
    for name in sorted(iter_submodule_names(path)):
        if import_as_extension:
            yield importlib.import_module("..." + name, __package__)
        else:
            yield importlib.import_module("." + name, path.name)


def iter_submodule_names(path, root=""):
    for _, module_name, is_package in pkgutil.iter_modules([str(path)]):
        if is_package:
            sub_path = path / module_name
            sub_root = root + module_name + "."
            yield from iter_submodule_names(sub_path, sub_root)
        else:
            yield root + module_name


# Find classes to register
#################################################

def get_ordered_classes_to_register(modules):
    return toposort(get_register_deps_dict(modules))


def get_framework_classes(modules):
    base_types = get_framework_base_classes()
    all_framework_classes = set()
    for cls in get_classes_in_modules(modules):
        if any(base in base_types for base in cls.__mro__[1:]):
            all_framework_classes.add(cls)
    return all_framework_classes


def get_register_deps_dict(modules):
    my_classes = set(iter_my_classes(modules))
    my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}

    deps_dict = {}
    for cls in my_classes:
        deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname))
    return deps_dict


def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
    yield from iter_my_deps_from_annotations(cls, my_classes)
    yield from iter_my_deps_from_inheritance(cls, my_classes)
    yield from iter_my_deps_from_parent_id(cls, my_classes_by_idname)


def iter_my_deps_from_annotations(cls, my_classes):
    # Fix for https://github.com/xzhuah/BlenderAddonPackageTool/issues/15
    import sys
    module = sys.modules.get(cls.__module__)
    globalns = module.__dict__ if module else {}
    for value in typing.get_type_hints(cls, globalns, {}).values():
        dependency = get_dependency_from_annotation(value)
        if dependency is not None:
            if dependency in my_classes:
                yield dependency


def iter_my_deps_from_inheritance(cls, my_classes):
    for base_cls in cls.__mro__[1:]:
        if base_cls in my_classes:
            yield base_cls


def get_dependency_from_annotation(value):
    if blender_version >= (2, 93):
        # Avoid checking private types
        # if isinstance(value, bpy.props._PropertyDeferred):
        #     return value.keywords.get("type")
        if hasattr(value, "keywords") and isinstance(value.keywords, dict):
            return value.keywords.get("type")
    else:
        if isinstance(value, tuple) and len(value) == 2:
            if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
                return value[1]["type"]
    return None


def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
    if bpy.types.Panel in cls.__mro__[1:]:
        parent_idname = getattr(cls, "bl_parent_id", None)
        if parent_idname is not None:
            parent_cls = my_classes_by_idname.get(parent_idname)
            if parent_cls is not None:
                yield parent_cls


def iter_my_classes(modules):
    base_types = get_register_base_types()
    for cls in get_classes_in_modules(modules):
        if any(base in base_types for base in cls.__mro__[1:]):
            if not getattr(cls, "is_registered", False):
                yield cls


def get_classes_in_modules(modules):
    classes = set()
    for module in modules:
        for cls in iter_classes_in_module(module):
            classes.add(cls)
    return classes


def iter_classes_in_module(module):
    for value in module.__dict__.values():
        if inspect.isclass(value):
            yield value


def get_register_base_types():
    names = [
        "Panel", "Operator", "PropertyGroup",
        "AddonPreferences", "Header", "Menu",
        "Node", "NodeSocket", "NodeTree",
        "UIList", "RenderEngine",
        "Gizmo", "GizmoGroup",
        "FileHandler",
    ]
    return {getattr(bpy.types, name) for name in names if hasattr(bpy.types, name)}


def get_framework_base_classes():
    return {ExpandableUi}


# Find order to register to solve dependencies
#################################################

def toposort(deps_dict):
    # Disallow circular dependencies
    if deps_dict:
        test_deps = {k: v.copy() for k, v in deps_dict.items()}
        for _ in range(len(deps_dict)):
            resolved = [k for k, v in test_deps.items() if not v]
            if not resolved:
                # 避免循环依赖
                raise ValueError(f"Circular dependency detected: {list(test_deps.keys())}")
            for k in resolved:
                del test_deps[k]
            for v in test_deps.values():
                v -= set(resolved)
            if not test_deps:
                break

    sorted_list = []
    sorted_values = set()
    while len(deps_dict) > 0:
        unsorted = []
        # class with no dependencies
        independent = []
        for value, deps in deps_dict.items():
            if len(deps) == 0:
                independent.append(value)
            else:
                unsorted.append(value)

        # sort no dependencies by _reg_order
        independent.sort(key=lambda x: getattr(x, "_reg_order", float('inf')))
        # add to sorted list
        for value in independent:
            sorted_list.append(value)
            sorted_values.add(value)

        deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
    return sorted_list


def register_framework_class(cls):
    if issubclass(cls, ExpandableUi):
        if hasattr(bpy.types, cls.target_id):
            if cls.expand_mode == "APPEND":
                getattr(bpy.types, cls.target_id).append(cls.draw)
            elif cls.expand_mode == "PREPEND":
                getattr(bpy.types, cls.target_id).prepend(cls.draw)
            else:
                raise ValueError(f"Invalid expand_mode: {cls.expand_mode}")
        else:
            print(f"Warning: Target ID not found: {cls.target_id}")


def unregister_framework_class(cls):
    if issubclass(cls, ExpandableUi):
        if hasattr(bpy.types, cls.target_id):
            getattr(bpy.types, cls.target_id).remove(cls.draw)


# support adding properties in a declarative way
def add_properties(property_dict: dict[typing.Any, dict[str, typing.Any]]):
    for cls, properties in property_dict.items():
        for name, prop in properties.items():
            setattr(cls, name, prop)


# support removing properties in a declarative way
def remove_properties(property_dict: dict[typing.Any, dict[str, typing.Any]]):
    for cls, properties in property_dict.items():
        for name in properties.keys():
            if hasattr(cls, name):
                delattr(cls, name)


================================================
FILE: common/class_loader/module_installer.py
================================================
# Notice: Please do not use functions in this file for developing your Blender Addons, this file is for internal use of
# the framework, it contains some modules that Blender officially prohibits using in Addons. Such as sys
# 注意:请不要在Blender中使用此文件中的函数,此文件用于框架内部使用,包含了一些Blender官方禁止在插件中使用的模块 如sys
import importlib.metadata
import importlib.util
import os
import platform
import subprocess
import sys


def install(package):
    subprocess.check_call([sys.executable, "-m", "pip", "install", package])


def has_module(module_name):
    try:
        return importlib.util.find_spec(module_name) is not None
    except Exception as e:
        return False


def is_package_installed(package_name):
    try:
        importlib.metadata.version(package_name)
        return True
    except importlib.metadata.PackageNotFoundError:
        return False


def install_if_missing(package):
    if not has_module(package):
        install(package)


def get_blender_version(blender_exe_path):
    try:
        # Run the Blender executable with --version
        result = subprocess.run(
            [blender_exe_path, "--version"],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        # Check if the process was successful
        if result.returncode == 0:
            output = result.stdout
            # Parse the version from the output
            for line in output.splitlines():
                if line.startswith("Blender"):
                    # Extract version number
                    return line.split()[1]  # e.g., "3.6.2"
        else:
            print("Error running Blender:", result.stderr)
    except Exception as e:
        print(f"An error occurred when trying to determine Blender version: {e}")
    return None


def extract_blender_version(blender_exe_path: str):
    """Extract the first two version numbers from the Blender executable path."""
    version_str = get_blender_version(blender_exe_path)
    try:
        return ".".join(version_str.split(".")[0:2])
    except Exception as e:
        print(f"An error occurred when trying to extract Blender version from {version_str}: {e}")
    return None


def install_fake_bpy(blender_path: str):
    blender_version = extract_blender_version(blender_path)
    if blender_version is None:
        print("Blender version not found in path: " + blender_path)
        blender_version = "latest"
    desired_module = "fake-bpy-module-" + blender_version
    if has_module("bpy"):
        if not is_package_installed(desired_module):
            print("Your fake bpy module is different from the current blender version! You might need to update it.")
        return
    else:
        print("Installing fake bpy module for Blender version: " + blender_version)
        try:
            install(desired_module)
        except Exception as e:
            if desired_module != "fake-bpy-module-latest":
                print(
                    "Failed to install fake bpy module for Blender version: " + blender_version + "! Trying to install the latest version.")
                install("fake-bpy-module-latest")


def normalize_blender_path_by_system(blender_path: str):
    if is_mac():
        if blender_path.endswith(".app"):
            blender_path = os.path.join(blender_path, "Contents/MacOS/Blender")
    return blender_path


def default_blender_addon_path(blender_path: str):
    blender_path = normalize_blender_path_by_system(blender_path)
    blender_version = extract_blender_version(blender_path)
    assert blender_version is not None, "Can not detect Blender version with " + blender_path + "!"
    if is_windows() or is_linux():
        new_path = os.path.join(os.path.dirname(blender_path), blender_version, "scripts", "addons_core")
        if os.path.exists(new_path):
            return new_path
        return os.path.join(os.path.dirname(blender_path), blender_version, "scripts", "addons")
    elif is_mac():
        user_path = os.path.expanduser("~")
        return os.path.join(user_path, f"Library/Application Support/Blender/{blender_version}/scripts/addons")
    else:
        raise Exception(
            "This Framework is currently not compatible with your operating system! Please use Windows, MacOS or Linux.")


def is_windows():
    return platform.system() == "Windows"


def is_linux():
    return platform.system() == "Linux"


def is_mac():
    return platform.system() == "Darwin"


================================================
FILE: common/i18n/__init__.py
================================================


================================================
FILE: common/i18n/dictionary.py
================================================
common_dictionary = {
    "zh_CN": {
        # ("*", "translation"): "翻译",
    }
}

common_dictionary["zh_HANS"] = common_dictionary["zh_CN"]


# preprocess dictionary
def preprocess_dictionary(dictionary):
    for key in dictionary:
        invalid_items = {}
        for translate_key in dictionary[key]:
            if isinstance(translate_key, str):
                invalid_items[translate_key] = dictionary[key][translate_key]
        for invalid_item in invalid_items:
            translation = invalid_items[invalid_item]
            dictionary[key][("*", invalid_item)] = translation
            dictionary[key][("Operator", invalid_item)] = translation
            del dictionary[key][invalid_item]
    return dictionary


================================================
FILE: common/i18n/i18n.py
================================================
import bpy

# Get the language code when addon start up
__language_code__ = bpy.context.preferences.view.language

from .dictionary import common_dictionary

__dictionary__ = common_dictionary


# Dictionary for translation: https://docs.blender.org/api/current/bpy.app.translations.html
# {
#     "en_US": {
#         ("*", code1): "translation1",
#         ("Operator", code2): "translation2",
#     },
#     "zh_CN": {
#         ("*", key): "翻译",
#         ("*, key2): "翻译2"
#     }
# }

# Set a new dictionary for translation
def set_dictionary(new_dictionary: dict[str, dict[tuple, str]]):
    global __dictionary__
    __dictionary__ = new_dictionary


# Load additional dictionary for translation
def load_dictionary(additional_dictionary: dict[str, dict[tuple, str]]):
    global __dictionary__
    for key in additional_dictionary:
        if key in __dictionary__:
            __dictionary__[key].update(additional_dictionary[key])
        else:
            __dictionary__[key] = {}
            __dictionary__[key].update(additional_dictionary[key])


# 在需要拼接字符串的地方使用i18n函数
def i18n(content: str) -> str:
    global __language_code__, __dictionary__
    __language_code__ = bpy.context.preferences.view.language
    if __language_code__ not in __dictionary__:
        return content
    tuple_contents = [("*", content), ("Operator", content)]
    for tuple_content in tuple_contents:
        if tuple_content in __dictionary__[__language_code__]:
            return __dictionary__[__language_code__][tuple_content]
    for key in __dictionary__[__language_code__]:
        if key[1] == content:
            return __dictionary__[__language_code__][key]
    return content


================================================
FILE: common/io/FileManagerClient.py
================================================
import hashlib
import os
from os import listdir


def get_all_filename(folder_path: str) -> list:
    if os.path.exists(folder_path):
        return [f for f in listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
    else:
        return []


def get_all_subfolder(folder_path: str) -> list:
    return [f for f in listdir(folder_path) if os.path.isdir(os.path.join(folder_path, f))]


# return true if path_a is a subdirectory under path_b
def is_subdirectory(path_a, path_b) -> bool:
    path_a = os.path.abspath(path_a)
    path_b = os.path.abspath(path_b)
    return os.path.commonpath([path_b]) == os.path.commonpath([path_a, path_b])


def is_filename_postfix_in(filename: str, target_set: set):
    if target_set is None or len(target_set) == 0:
        return True
    for postfix in target_set:
        if filename.lower().endswith(postfix.lower()):
            return True
    return False


# 搜索文件夹下所有文件 post_filter为后缀名集合 全小写
def search_files(folder_path: str, post_filter: set) -> list:
    def __depth_first_search_files_helper__(current_folder: str, pre_result: list):
        for filename in get_all_filename(current_folder):
            if is_filename_postfix_in(filename, post_filter):
                pre_result.append(os.path.join(current_folder, filename))
        all_folders = get_all_subfolder(current_folder)
        for folder in all_folders:
            __depth_first_search_files_helper__(os.path.join(current_folder, folder), pre_result)

    all_file = []
    __depth_first_search_files_helper__(folder_path, all_file)
    return all_file


def get_md5(filename):
    return hashlib.md5(open(filename, 'rb').read()).hexdigest()


def get_md5_folder(folder_path: str) -> str:
    all_files = search_files(folder_path, set())
    md5_content = ""
    for file in all_files:
        md5_content += get_md5(file)
    return hashlib.md5(md5_content.encode("utf-8")).hexdigest()


def read_utf8(filepath: str) -> str:
    with open(filepath, mode="r", encoding="utf-8") as f:
        return f.read()


def read_utf8_in_lines(filepath: str) -> list[str]:
    with open(filepath, mode="r", encoding="utf-8") as f:
        return f.readlines()


def write_utf8(filepath: str, content: str):
    with open(filepath, encoding="utf-8", mode="w") as f:
        f.write(content)


def write_utf8_in_lines(filepath: str, content: list[str]):
    with open(filepath, encoding="utf-8", mode="w") as f:
        f.writelines(content)


================================================
FILE: common/io/__init__.py
================================================


================================================
FILE: common/types/__init__.py
================================================


================================================
FILE: common/types/framework.py
================================================
import bpy


def is_extension():
    # Blender extension package starts with "bl_ext."
    # https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html#extensions-and-namespace
    return str(__package__).startswith("bl_ext.")


# This is a helper base class for you to expand native Blender UI
class ExpandableUi:
    # ID of the target panel.menu to be expanded to
    target_id: str
    # mode of expansion, either "PREPEND" or "APPEND"
    expand_mode: str = "APPEND"

    def draw(self, context: bpy.types.Context):
        raise NotImplementedError("draw method must be implemented")


def reg_order(order_value: int):
    """
    This decorator is used to specify the relative registration order of a class. The class with lower order value will
    be registered first, class without this decorator will be registered last.
    Notice it still respect the dependencies between classes. Only classes with no dependencies relationship will be
    sorted by order value.
    Notice, for UI classes, updating the order won't take effect in real-time during testing, you need to also update
    the bl_idname of the class to let Blender clean up the drawing cache.
    这个装饰器用于指定类的相对注册顺序。具有较低顺序值的类将首先注册,没有此装饰器的类将最后注册。
    请注意,它仍然尊重类之间的依赖关系。只有没有依赖关系的类才会按顺序值排序。
    请注意,对于UI类,更新顺序不会在测试期间实时生效,您还需要更新类的bl_idname,以便Blender清除绘图缓存。
    """

    def class_decorator(cls):
        cls._reg_order = order_value
        return cls

    return class_decorator


================================================
FILE: create.py
================================================
from framework import new_addon
from main import ACTIVE_ADDON

# 创建前请修改以下参数

# The name of the addon to be created, this name is defined in the config.py of the addon as __addon_name__
# 插件的config.py文件中定义的插件名称 __addon_name__

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('addon', default=ACTIVE_ADDON, nargs='?', help='addon name')
    args = parser.parse_args()
    new_addon(args.addon)


================================================
FILE: framework.py
================================================
import ast
import atexit
import os
import re
import shutil
import subprocess
import sys
import threading
import time
from datetime import datetime
from pathlib import Path

from common.class_loader.module_installer import install_if_missing, install_fake_bpy
from common.io.FileManagerClient import search_files, read_utf8, write_utf8, is_subdirectory, get_md5_folder, \
    read_utf8_in_lines, write_utf8_in_lines
from main import PROJECT_ROOT, BLENDER_ADDON_PATH, BLENDER_EXE_PATH, DEFAULT_RELEASE_DIR, TEST_RELEASE_DIR, IS_EXTENSION

try:
    # added in python3.11
    import tomllib
except ImportError:
    # for python3.10 and below
    install_if_missing("toml")
    import toml

# Following variables are used internally in the framework according to some protocols defined by Blender or
# the framework itself. Do not change them unless you know what you are doing.
_addon_namespace_pattern = re.compile("^[a-zA-Z]+[a-zA-Z0-9_]*$")
_import_module_pattern = re.compile("from ([a-zA-Z_][a-zA-Z0-9_.]*) import (.+)")
_relative_import_pattern = re.compile(r'^\s*(from\s+(\.+))(.*)$')
_absolute_import_pattern = re.compile(r'^\s*from\s+(\w+[\w.]*)\s+import\s+(.*)$')
_addon_md5__signature = "addon.txt"
_ADDON_MANIFEST_FILE = "blender_manifest.toml"
_WHEELS_PATH = "wheels"
# 默认使用的插件模板 不要轻易修改
_ADDON_TEMPLATE = "sample_addon"
_ADDONS_FOLDER = "addons"
_ADDON_ROOT = os.path.join(PROJECT_ROOT, _ADDONS_FOLDER)

# Install fake bpy module only when user have configured the blender executable path
# 仅在用户配置了Blender可执行文件路径时安装fake bpy模块 避免在非Blender环境下安装fake bpy模块(如CICD流程中)
if os.path.isfile(BLENDER_EXE_PATH):
    install_fake_bpy(BLENDER_EXE_PATH)


def new_addon(addon_name: str):
    new_addon_path = os.path.join(_ADDON_ROOT, addon_name)
    if os.path.exists(new_addon_path):
        raise ValueError("Addon already exists: " + addon_name)
    if not bool(_addon_namespace_pattern.match(addon_name)):
        raise ValueError("Invalid addon name: " + addon_name + " Please name it as a python package name")
    shutil.copytree(os.path.join(_ADDON_ROOT, _ADDON_TEMPLATE), new_addon_path)

    all_template_file = search_files(new_addon_path, {".py", ".toml"})
    for py_file in all_template_file:
        content = read_utf8(py_file).replace(_ADDON_TEMPLATE, addon_name)
        write_utf8(py_file, content)


def test_addon(addon_name, enable_watch=True):
    init_file = get_init_file_path(addon_name)
    if not enable_watch:
        print('Do not auto reload addon when file changed')
    start_test(init_file, addon_name, enable_watch=enable_watch)


def get_init_file_path(addon_name):
    # addon_name is the name defined in addon's config.py
    target_init_file_path = os.path.join(_ADDON_ROOT, addon_name, "__init__.py")
    if not os.path.exists(target_init_file_path):
        raise ValueError(f"Release failed: Addon {addon_name} not found.")
    return target_init_file_path


# The following code will be injected into the blender python environment to enable hot reload
# https://devtalk.blender.org/t/plugin-hot-reload-by-cleaning-sys-modules/20040
start_up_command = """
import bpy
from bpy.app.handlers import persistent
import os
import sys
existing_addon_md5 = ""
try:
    bpy.ops.preferences.addon_enable(module="{addon_name}")
except Exception as e:
    print("Addon enable failed:", e)

def watch_update_tick():
    global existing_addon_md5
    if os.path.exists("{addon_signature}"):
        with open("{addon_signature}", "r") as f:
            addon_md5 = f.read()
        if existing_addon_md5 == "":
            existing_addon_md5 = addon_md5
        elif existing_addon_md5 != addon_md5:
            print("Addon file changed, start to update the addon")
            try:
                bpy.ops.preferences.addon_disable(module="{addon_name}")
                all_modules = sys.modules
                all_modules = dict(sorted(all_modules.items(),key= lambda x:x[0])) #sort them
                for k,v in all_modules.items():
                    if k.startswith("{addon_name}"):
                        del sys.modules[k]
                bpy.ops.preferences.addon_enable(module="{addon_name}")
            except Exception as e:
                print("Addon update failed:", e)
            existing_addon_md5 = addon_md5
            print("Addon updated")
    return 1.0

@persistent
def register_watch_update_tick(dummy):
    print("Watching for addon update...")
    bpy.app.timers.register(watch_update_tick)

register_watch_update_tick(None)
bpy.app.handlers.load_post.append(register_watch_update_tick)
"""


def start_test(init_file, addon_name, enable_watch=True):
    update_addon_for_test(init_file, addon_name)
    test_addon_path = os.path.normpath(os.path.join(BLENDER_ADDON_PATH, addon_name))

    if not enable_watch:
        def exit_handler():
            if os.path.exists(test_addon_path):
                shutil.rmtree(test_addon_path)

        atexit.register(exit_handler)
        try:
            execute_blender_script(
                [BLENDER_EXE_PATH, "--python-use-system-env", "--python-expr",
                 f"import bpy\nbpy.ops.preferences.addon_enable(module=\"{addon_name}\")"], test_addon_path)
        finally:
            exit_handler()
        return

    # start_watch_for_update(init_file, addon_name)
    stop_event = threading.Event()
    thread = threading.Thread(target=start_watch_for_update, args=(init_file, addon_name, stop_event))
    thread.start()

    def exit_handler():
        stop_event.set()
        thread.join()
        if os.path.exists(test_addon_path):
            shutil.rmtree(test_addon_path)

    atexit.register(exit_handler)

    python_script = start_up_command.format(addon_name=addon_name,
                                            addon_signature=os.path.join(test_addon_path,
                                                                         _addon_md5__signature).replace("\\", "/"))

    try:
        execute_blender_script([BLENDER_EXE_PATH, "--python-use-system-env", "--python-expr", python_script],
                               test_addon_path)
    finally:
        exit_handler()


# This is the only corner case need to handle
_addon_on_init_file = os.path.abspath(os.path.join(PROJECT_ROOT, "__init__.py"))


def execute_blender_script(args, addon_path):
    process = subprocess.Popen(args, stderr=subprocess.PIPE, text=True, encoding="utf-8")
    try:
        for line in process.stderr:
            line: str
            if line.lstrip().startswith("File"):
                line = line.replace(addon_path, PROJECT_ROOT)
            sys.stderr.write(line)
    except KeyboardInterrupt:
        sys.stderr.write("interrupted, terminating the child process...\n")
    finally:
        process.terminate()
        process.wait()


def read_ext_config(addon_config_file):
    with open(addon_config_file, 'r', encoding='utf-8') as f:
        try:
            addon_config = tomllib.loads(f.read())
        except Exception as e:
            addon_config = toml.load(f)
    return addon_config


def release_addon(target_init_file, addon_name,
                  release_dir=DEFAULT_RELEASE_DIR,
                  need_zip=True,
                  is_extension=IS_EXTENSION,
                  with_timestamp=False,
                  with_version=False):
    # if release dir is under PROJECT_ROOT, it's not allowed
    if is_subdirectory(release_dir, PROJECT_ROOT):
        # 不要将插件发布目录设置在当前项目内
        raise ValueError("Invalid release dir:", release_dir,
                         "Please set a release/test dir outside the current workspace")

    if not bool(_addon_namespace_pattern.match(addon_name)):
        raise ValueError("InValid addon_name:", addon_name, "Please name it as a python package name")

    if is_extension:
        # 发布为扩展时,请确保您在config.py正确的定义了__addon_name__
        # Release as extension, please make sure you defined __addon_name__ correctly in config.py"
        # Make sure toml file exists
        addon_config_file = os.path.join(_ADDON_ROOT, addon_name, _ADDON_MANIFEST_FILE)
        if not os.path.isfile(addon_config_file):
            raise ValueError("Extension config file not found:", addon_config_file)

    if not os.path.isdir(release_dir):
        Path(release_dir).mkdir(parents=True, exist_ok=True)

    # remove the folder if already exists
    release_folder = os.path.join(release_dir, addon_name)
    if os.path.exists(release_folder):
        shutil.rmtree(release_folder)
    os.mkdir(release_folder)

    bootstrap_init_file = generate_bootstrap_init_file(addon_name, get_addon_info(target_init_file))
    write_utf8(os.path.join(release_folder, "__init__.py"), bootstrap_init_file)

    # shutil.copyfile(target_init_file, os.path.join(release_folder, "__init__.py"))
    # 将target_init_file同级的其他非py文件复制到发布目录 如 toml xml等可能跟插件有关的配置文件
    for file in os.listdir(os.path.dirname(target_init_file)):
        file_path = os.path.join(os.path.dirname(target_init_file), file)
        if os.path.isdir(file_path) or file.endswith(".py"):
            continue
        shutil.copy(file_path, release_folder)

    # 将插件文件夹复制到发布目录
    shutil.copytree(os.path.join(_ADDON_ROOT, addon_name), os.path.join(release_folder, _ADDONS_FOLDER, addon_name))
    shutil.copyfile(os.path.join(_ADDON_ROOT, "__init__.py"),
                    os.path.join(release_folder, _ADDONS_FOLDER, "__init__.py"))

    all_py_files = search_files(os.path.join(_ADDON_ROOT, addon_name), {".py"})
    # 对插件文件夹中的每一个py文件进行分析,找到每个py文件中依赖的其他py文件
    visited_py_files = set()
    for py_file in all_py_files:
        visited_py_files.add(os.path.abspath(py_file))
    # 注意不要漏掉__init__.py文件
    visited_py_files.add(os.path.abspath(os.path.join(_ADDON_ROOT, "__init__.py")))

    dependencies = find_all_dependencies(list(visited_py_files), PROJECT_ROOT)
    for dependency in dependencies:
        dependency = os.path.abspath(dependency)
        if dependency in visited_py_files:
            continue
        visited_py_files.add(dependency)
        target_path = os.path.join(release_folder, os.path.relpath(dependency, PROJECT_ROOT))
        if not os.path.exists(os.path.dirname(target_path)):
            os.makedirs(os.path.dirname(target_path))
        shutil.copy(dependency, os.path.join(release_folder, os.path.relpath(dependency, PROJECT_ROOT)))

    remove_pyc_files(release_folder)
    removed_path = 1
    while removed_path > 0:
        removed_path = remove_empty_folders(release_folder)

    # 必须先将绝对导入转换为相对导入,否则enhance_import_for_py_files一步会改变绝对导入的路径导致出错
    # convert absolute import to relative import if it's an extension
    if is_extension:
        for py_file in search_files(release_folder, {".py"}):
            convert_absolute_to_relative(py_file, release_folder)

    # 更新打包后的绝对导入路径:由于打包后文件夹的层级关系发生了变化,需要更新打包后的绝对导入路径
    enhance_import_for_py_files(release_folder)

    # enhance relative import for root __init__.py
    # enhance_relative_import_for_init_py(os.path.join(release_folder, "__init__.py"),
    #                                     _ADDONS_FOLDER, addon_name)

    # include wheel files when need to be zipped
    addon_config_file = os.path.join(_ADDON_ROOT, addon_name, _ADDON_MANIFEST_FILE)
    addon_config = {}
    if os.path.exists(addon_config_file) and is_extension:
        addon_config = read_ext_config(addon_config_file)
    if need_zip:
        # package whl files into extension
        if "wheels" in addon_config:
            wheel_files = addon_config["wheels"]
            if len(wheel_files) > 0:
                wheel_folder = os.path.join(release_folder, _WHEELS_PATH)
                os.mkdir(wheel_folder)
                for wheel_file in wheel_files:
                    # You much put the .whl file directly under the wheels folder, not in a subfolder
                    # 你必须将.whl文件直接放在wheels文件夹下,而不是在子文件夹中
                    assert wheel_file.startswith("./wheels/") and wheel_file.count("/") == 2
                    wheel_source = os.path.join(PROJECT_ROOT, wheel_file)
                    if not os.path.exists(wheel_source):
                        raise ValueError("Wheel file not found:", wheel_source,
                                         ". Please download the required wheel file to the wheels folder.")
                    shutil.copy(wheel_source, wheel_folder)

    real_addon_name = "{addon_name}".format(addon_name=release_folder)
    if is_extension:
        real_addon_name = f"{real_addon_name}_ext"
    if with_version:
        _version: str
        if not is_extension:
            bl_info = get_addon_info(target_init_file)
            if bl_info is not None:
                _version = '.'.join([str(x) for x in bl_info['version']])
            else:
                raise ValueError("bl_info not found in:", target_init_file)
        else:
            if "version" in addon_config:
                _version = addon_config["version"]
            else:
                raise ValueError("version not found in:", addon_config_file)
        real_addon_name = f"{real_addon_name}_V{_version}"
    if with_timestamp:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        real_addon_name = f"{real_addon_name}_{timestamp}"

    released_addon_path = os.path.abspath(os.path.join(release_dir, real_addon_name) + ".zip")
    # zip the addon
    if need_zip:
        zip_folder(release_folder, real_addon_name, is_extension)
        print("Add on released:", released_addon_path)

    return released_addon_path


def get_addon_info(filename: str):
    file_content = read_utf8(filename)
    try:
        parsed_ast = ast.parse(file_content)
        for node in ast.walk(parsed_ast):
            if isinstance(node, ast.Assign):
                for target in node.targets:
                    if isinstance(target, ast.Name) and target.id == "bl_info":
                        return ast.literal_eval(node.value)
    except Exception as e:
        return None


def generate_bootstrap_init_file(addon_name: str, bl_info: dict):
    bootstrap_init_file_template = """from .addons.{addon_name} import register as addon_register, unregister as addon_unregister

bl_info = {bl_info}

def register():
    addon_register()

def unregister():
    addon_unregister()

    """
    bl_info_str = (
            "{\n"
            + ",\n".join(
        f'    "{key}": {repr(value)}'
        for key, value in bl_info.items()
    )
            + "\n}"
    )
    return bootstrap_init_file_template.format(addon_name=addon_name, bl_info=bl_info_str)


# pyc files are auto generated, need to be removed before release
def remove_pyc_files(release_folder: str):
    all_pyc_file = search_files(release_folder, {"pyc"})
    for pyc_file in all_pyc_file:
        os.remove(pyc_file)


def remove_empty_folders(root_path):
    all_folder_to_remove = []
    for root, dirnames, filenames in os.walk(root_path, topdown=False):
        for dirname in dirnames:
            dir_to_check = os.path.join(root, dirname)
            if not os.listdir(dir_to_check):
                all_folder_to_remove.append(dir_to_check)
    for folder in all_folder_to_remove:
        shutil.rmtree(folder)
    return len(all_folder_to_remove)


# Zip the folder in a way that blender can recognize it as an addon.
def zip_folder(target_root, output_zip_file, is_extension):
    if is_extension:
        shutil.make_archive(output_zip_file, 'zip', Path(target_root))
    else:
        shutil.make_archive(output_zip_file, 'zip', Path(target_root).parent, base_dir=os.path.basename(target_root))


def find_imported_modules(file_path):
    root = ast.parse(read_utf8(file_path), filename=file_path)

    imported_modules = set()
    for node in ast.walk(root):
        if isinstance(node, ast.Import):
            for alias in node.names:
                imported_modules.add(alias.name)
        elif isinstance(node, ast.ImportFrom):
            if node.module:
                module_name = node.module
                imported_modules.add(module_name)
            for alias in node.names:
                if node.module:
                    imported_modules.add(f"{node.module}.{alias.name}")
                else:
                    imported_modules.add(alias.name)
    return imported_modules


def resolve_module_path(module_name, base_path, project_root):
    if not module_name.endswith(".*"):
        # Handle import all
        module_path = module_name.replace('.', '/')
        module_path = os.path.join(project_root, module_path)
        if os.path.isdir(module_path):
            module_path = os.path.join(module_path, '__init__.py')
            return [module_path]
        elif os.path.isfile(module_path + '.py'):
            module_path = module_path + '.py'
            return [module_path]
        else:
            if "." not in module_name:
                # most likely a standard library module
                # 有一种可能是相对导入 from . import xxx, from .. import xxx 等
                # 这种情况下需要根据当前文件的路径来解析 看module_name.py是否存在于当前文件的同级目录或者父级目录
                # 从base_path开始向上查找,直到找到module_name.py或者到达project_root
                search_path = os.path.dirname(base_path)
                potential_result = []
                while is_subdirectory(search_path, project_root):
                    possible_path = os.path.join(search_path, module_name + '.py')
                    if os.path.isfile(possible_path):
                        potential_result.append(possible_path)
                    search_path = os.path.dirname(search_path)
                return potential_result
            current_search_dir = os.path.dirname(base_path)
            while is_subdirectory(current_search_dir, project_root):
                module_path = module_name.replace('.', '/')
                module_path = os.path.join(current_search_dir, module_path)
                if os.path.isdir(module_path):
                    module_path = os.path.join(module_path, '__init__.py')
                    return [module_path]
                elif os.path.isfile(module_path + '.py'):
                    module_path = module_path + '.py'
                    return [module_path]
                current_search_dir = os.path.dirname(current_search_dir)
            return []
    else:
        module_name = module_name[:-2]
        module_path = module_name.replace('.', '/')
        possible_root_path = os.path.join(project_root, module_path)
        if os.path.isdir(possible_root_path):
            possible_root_path = os.path.join(possible_root_path, '__init__.py')
            return [possible_root_path]
        elif os.path.isfile(possible_root_path + '.py'):
            possible_root_path = possible_root_path + '.py'
            return [possible_root_path]
        else:
            current_search_dir = os.path.dirname(base_path)
            while is_subdirectory(current_search_dir, project_root):

                possible_root_path = os.path.join(current_search_dir, module_path)
                if os.path.isdir(possible_root_path):
                    possible_root_path = os.path.join(possible_root_path, '__init__.py')
                    return [possible_root_path]
                elif os.path.isfile(possible_root_path + '.py'):
                    possible_root_path = possible_root_path + '.py'
                    return [possible_root_path]
                current_search_dir = os.path.dirname(current_search_dir)
            return []


def find_all_dependencies(file_paths: list, project_root: str):
    dependencies = set()
    to_process = file_paths.copy()
    processed = set()

    while to_process:
        current_file = os.path.abspath(to_process.pop())
        if current_file in processed:
            continue

        processed.add(current_file)
        dependencies.add(current_file)

        try:
            imported_modules = find_imported_modules(current_file)
        except SyntaxError as e:
            raise SyntaxError(f"Syntax error in file {current_file}: {e}")

        # 以下代码会将除了当前目标插件文件夹以外的其他被引用的文件夹中的__init__.py文件也加入到依赖中,使之成为有效的模块,从而将其中的Blender
        # 类也加入到自动注册的范围中,一般来说,我们引用外部文件夹的目的是复用其内部函数,而非将插件外部模块中定义的Operator,Panel等元素
        # 直接加到当前插件中(如果需要使用其他插件的这些元素,更好的做法是将其直接存放到你的插件文件夹内),因此注释掉,如果您有特殊需求,可以取消注释
        # The following code will add __init__.py files in other
        # referenced folders to the dependencies, in addition to the current ACTIVE ADDON ,making those folders valid
        # modules and thus classes in them will be added the scope of automatic class registration. (The
        # auto_load.py) It is commented out because usually we just want to reference reusable functions from
        # modules outside the current addon Instead of directly adding their Operator's Panels into your own addon. (
        # If you really want to do that, include them as sub package of your own addon would be a better option). But
        # If you have special requirements, you can uncomment it.

        # potential_init_file = os.path.abspath(os.path.join(os.path.dirname(current_file), '__init__.py'))
        # while is_subdirectory(os.path.dirname(potential_init_file),
        #                       project_root) and potential_init_file != os.path.abspath(
        #         os.path.join(project_root, "__init__.py")):
        #     if os.path.exists(potential_init_file) and potential_init_file not in processed:
        #         to_process.append(potential_init_file)
        #         dependencies.add(potential_init_file)
        #     potential_init_file = os.path.abspath(
        #         os.path.join(os.path.dirname(os.path.dirname(potential_init_file)), '__init__.py'))

        for module in imported_modules:
            module_path = resolve_module_path(module, current_file, project_root)
            if len(module_path) > 0:
                for each_module_path in module_path:
                    each_module_path = os.path.abspath(each_module_path)
                    if each_module_path not in processed:
                        to_process.append(each_module_path)

    return dependencies


def enhance_import_for_py_files(addon_dir: str):
    namespace = os.path.basename(addon_dir)
    all_py_modules = find_all_py_modules(addon_dir)
    all_py_file = search_files(addon_dir, {".py"})
    for py_file in all_py_file:
        hasUpdated = False
        content = read_utf8(py_file)
        for module_path in _import_module_pattern.finditer(content):
            original_module_path = module_path.groups()[0]
            if original_module_path in all_py_modules:
                hasUpdated = True
                content = content.replace("from " + original_module_path + " import",
                                          "from " + namespace + "." + original_module_path + " import")
        if hasUpdated:
            write_utf8(py_file, content)


def convert_absolute_to_relative(file_path: str, project_root: str):
    """
    Convert all absolute imports to relative imports in a Python file.
    Notice this does not handle import like
    import xxx.yyy.zzz as zzz
    在开发扩展时,不要用这种方式导入项目内的模块,这种方式导入的模块无法被转换为相对导入

    Args:
        file_path (str): Path to the Python file to modify.
        project_root (str): Root directory of the project.
    """
    # Normalize paths
    file_path = os.path.abspath(file_path)
    project_root = os.path.abspath(project_root)

    lines = read_utf8_in_lines(file_path)

    modified_lines = []
    changed = False

    for line in lines:
        # help skipping expensive path check
        stripped_line = line.strip()
        if (not stripped_line.startswith("from ")) or stripped_line.startswith("from ."):
            # Leave non-import lines unchanged
            modified_lines.append(line)
            continue
        match = _absolute_import_pattern.match(line)
        if match:
            # get whitespace before the import statement
            leading_space = line[:line.index("from")]
            absolute_module = match.group(1)
            import_items = match.group(2)
            # Check if the absolute module is within the project
            absolute_module_path = absolute_module.replace('.', os.sep)
            full_module_path = os.path.join(project_root, absolute_module_path)
            if os.path.exists(full_module_path) or os.path.exists(f"{full_module_path}.py"):
                # Calculate the relative import path

                target_relative_path = os.path.relpath(
                    os.path.join(project_root, absolute_module_path),
                    os.path.dirname(file_path)
                )
                # Count the levels for leading dots
                levels_up = target_relative_path.count("..") + 1
                leading_dots = '.' * levels_up

                # Build the relative import line
                target_relative_path = target_relative_path.strip("." + os.sep)
                relative_import_line = leading_space + f"from {leading_dots}{target_relative_path.replace(os.sep, '.')} import {import_items}\n"
                if relative_import_line != line:
                    modified_lines.append(relative_import_line)
                    changed = True
                    continue
            else:
                # Leave non-matching lines unchanged
                modified_lines.append(line)
        else:
            # Leave non-matching lines unchanged
            modified_lines.append(line)
        # print(f"not match {line} in {timer() - start3} seconds")

    # Write the modified content back to the file if changes were made
    if changed:
        write_utf8_in_lines(file_path, modified_lines)


def find_all_py_modules(root_dir: str) -> set:
    all_py_modules = set()
    all_py_file = search_files(root_dir, {".py"})
    for py_file in all_py_file:
        rel_path = str(os.path.relpath(py_file, root_dir))
        modules = rel_path.replace("__init__.py", "").replace(".py", "").split(os.path.sep)
        if len(modules[-1]) == 0:
            modules = modules[0:-1]

        module_name = ""
        for i in range(len(modules)):
            module_name += modules[i] + "."
            all_py_modules.add(module_name[0:-1])
    return all_py_modules


def start_watch_for_update(init_file, addon_name, stop_event: threading.Event):
    install_if_missing("watchdog")
    from watchdog.events import FileSystemEventHandler
    from watchdog.observers import Observer

    class FileUpdateHandler(FileSystemEventHandler):
        def __init__(self):
            super(FileUpdateHandler, self).__init__()
            self.has_update = False

        def on_any_event(self, event):
            source_path = event.src_path
            if source_path.endswith(".py"):
                self.has_update = True

        def clear_update(self):
            self.has_update = False

    path = PROJECT_ROOT
    event_handler = FileUpdateHandler()
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)
    observer.start()

    try:
        while not stop_event.is_set():
            time.sleep(1)
            if event_handler.has_update:
                try:
                    update_addon_for_test(init_file, addon_name)
                    event_handler.clear_update()
                except Exception as e:
                    print(e)
                    print(
                        "Addon updated failed: Please make sure no other process is"
                        " using the addon folder. You might need to restart the test to update the addon in Blender.")
        print("Stop watching for update...")

    except KeyboardInterrupt:
        observer.stop()
        observer.join()


def update_addon_for_test(init_file, addon_name):
    if BLENDER_ADDON_PATH is None:
        # 无法得到Blender插件路径 请检查在main.py或config.ini中的配置
        raise ValueError(
            "Could not find Blender addon installation path. Please check the configuration in main.py or config.ini")
    addon_path = release_addon(init_file, addon_name, with_timestamp=False,
                               is_extension=IS_EXTENSION,
                               release_dir=TEST_RELEASE_DIR, need_zip=False)
    executable_path = os.path.join(os.path.dirname(addon_path), addon_name)

    test_addon_path = os.path.join(BLENDER_ADDON_PATH, addon_name)
    if os.path.exists(test_addon_path):
        shutil.rmtree(test_addon_path)
    shutil.copytree(executable_path, test_addon_path)

    # write an MD5 to the addon folder to inform the addon content has been changed
    addon_md5 = get_md5_folder(executable_path)
    write_utf8(os.path.join(test_addon_path, _addon_md5__signature), addon_md5)


================================================
FILE: main.py
================================================
# BlenderAddonPackageTool - A framework for developing multiple blender addons in a single workspace
# Copyright (C) 2024 Xinyu Zhu

import os
from configparser import ConfigParser

from common.class_loader.module_installer import default_blender_addon_path, normalize_blender_path_by_system

# The name of current active addon to be created, tested or released
# 要创建、测试或发布的当前活动插件的名称
ACTIVE_ADDON = "sample_addon"

# The path of the blender executable. Blender2.93 is the minimum version required
# Blender可执行文件的路径,Blender2.93是所需的最低版本
BLENDER_EXE_PATH = "C:/software/general/Blender/blender-3.6.0-windows-x64/blender.exe"

# Linux example Linux示例
# BLENDER_EXE_PATH = "/usr/local/blender/blender-3.6.0-linux-x64/blender"

# MacOS examplenotice "/Contents/MacOS/Blender" will be appended automatically if you didn't write it explicitly
# MacOS示例 框架会自动附加"/Contents/MacOS/Blender" 所以您不必写出
# BLENDER_EXE_PATH = "/Applications/Blender/blender-3.6.0-macOS/Blender.app"

# Are you developing an extension(for Blender4.2) instead of legacy addon?
# https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html
# The framework will convert absolute import to relative import when packaging the extension.
# Make sure to update __addon_name__ in config.py if you are migrating from legacy addon to extension.
# 是否是面向Blender4.2以后的扩展而不是传统插件?
# https://docs.blender.org/manual/en/latest/advanced/extensions/addons.html
# 在打包扩展时,框架会将绝对导入转换为相对导入。如果你从传统插件迁移到扩展,请确保更新config.py中的__addon_name__
IS_EXTENSION = False

# You can override the default path by setting the path manually
# 您可以通过手动设置路径来覆盖默认插件安装路径 或者在config.ini中设置
# BLENDER_ADDON_PATH = "C:/software/general/Blender/Blender3.5/3.5/scripts/addons/"
BLENDER_ADDON_PATH = None
if os.path.exists(BLENDER_EXE_PATH):
    BLENDER_ADDON_PATH = default_blender_addon_path(BLENDER_EXE_PATH)

PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

# 若存在config.ini则从其中中读取配置
CONFIG_FILEPATH = os.path.join(PROJECT_ROOT, 'config.ini')

# The default release dir. Must not within the current workspace
# 插件发布的默认目录,不能在当前工作空间内
DEFAULT_RELEASE_DIR = os.path.join(PROJECT_ROOT, "../addon_release/")

# The default test release dir. Must not within the current workspace
# 测试插件发布的默认目录,不能在当前工作空间内
TEST_RELEASE_DIR = os.path.join(PROJECT_ROOT, "../addon_test/")

if os.path.isfile(CONFIG_FILEPATH):
    configParser = ConfigParser()
    configParser.read(CONFIG_FILEPATH, encoding='utf-8')

    if configParser.has_option('blender', 'exe_path'):
        BLENDER_EXE_PATH = configParser.get('blender', 'exe_path')
        # The path of the blender addon folder
        # 同时更改Blender插件文件夹的路径
        BLENDER_ADDON_PATH = default_blender_addon_path(BLENDER_EXE_PATH)

    if configParser.has_option('blender', 'addon_path') and configParser.get('blender', 'addon_path'):
        BLENDER_ADDON_PATH = configParser.get('blender', 'addon_path')

    if configParser.has_option('default', 'addon') and configParser.get('default', 'addon'):
        ACTIVE_ADDON = configParser.get('default', 'addon')

    if configParser.has_option('default', 'is_extension') and configParser.get('default', 'is_extension'):
        IS_EXTENSION = configParser.getboolean('default', 'is_extension')

    if configParser.has_option('default', 'release_dir') and configParser.get('default', 'release_dir'):
        DEFAULT_RELEASE_DIR = configParser.get('default', 'release_dir')

    if configParser.has_option('default', 'test_release_dir') and configParser.get('default', 'test_release_dir'):
        TEST_RELEASE_DIR = configParser.get('default', 'test_release_dir')

BLENDER_EXE_PATH = normalize_blender_path_by_system(BLENDER_EXE_PATH)

# If you want to override theBLENDER_ADDON_PATH(the path to install addon during testing), uncomment the following line and set the path manually.
# 如果要覆盖BLENDER_ADDON_PATH(测试插件安装路径),请取消下一行的注释并手动设置路径
# BLENDER_ADDON_PATH = ""

# Could not find the blender addon path, raise error. Please set BLENDER_ADDON_PATH manually.
# 未找到Blender插件路径,引发错误 请手动设置BLENDER_ADDON_PATH
if os.path.exists(BLENDER_EXE_PATH) and (not BLENDER_ADDON_PATH or not os.path.exists(BLENDER_ADDON_PATH)):
    raise ValueError("Blender addon path not found: " + BLENDER_ADDON_PATH, "Please set the correct path in config.ini")


================================================
FILE: release.py
================================================
from framework import get_init_file_path, release_addon
from main import ACTIVE_ADDON, IS_EXTENSION

# 发布前请修改ACTIVE_ADDON参数

# The name of the addon to be released, this name is defined in the config.py of the addon as __addon_name__
# 插件的config.py文件中定义的插件名称 __addon_name__


if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('addon', default=ACTIVE_ADDON, nargs='?', help='addon name')
    parser.add_argument('--is_extension', default=IS_EXTENSION, action='store_true', help='If true, package the addon '
                                                                                          'as extension, framework '
                                                                                          'will convert absolute '
                                                                                          'import to relative import '
                                                                                          'for you and will take care '
                                                                                          'of packaging the '
                                                                                          'extension. Default is the '
                                                                                          'value of IS_EXTENSION')
    parser.add_argument('--disable_zip', default=False, action='store_true', help='If true, release the addon into a '
                                                                                  'plain folder and do not zip it '
                                                                                  'into an installable package, '
                                                                                  'useful if you want to add more '
                                                                                  'files and zip by yourself.')
    parser.add_argument('--with_version', default=False, action='store_true', help='Append the addon version number ('
                                                                                   'as specified in bl_info) to the '
                                                                                   'released zip file name.')
    parser.add_argument('--with_timestamp', default=False, action='store_true', help='Append a timestamp to the zip '
                                                                                     'file name.')
    args = parser.parse_args()
    release_addon(target_init_file=get_init_file_path(args.addon),
                  addon_name=args.addon,
                  need_zip=not args.disable_zip,
                  is_extension=args.is_extension,
                  with_timestamp=args.with_timestamp,
                  with_version=args.with_version,
                  )


================================================
FILE: test.py
================================================
from framework import test_addon
from main import ACTIVE_ADDON

# 测试前请修改ACTIVE_ADDON参数

# The name of the addon to be tested, this name is defined in the config.py of the addon as __addon_name__
# 插件的config.py文件中定义的插件名称 __addon_name__

if __name__ == '__main__':
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('addon', default=ACTIVE_ADDON, nargs='?', help='addon name')
    parser.add_argument('--disable_watch', default=False, action='store_true', help='Do not reload addon when file '
                                                                                    'changed')
    args = parser.parse_args()
    test_addon(args.addon, enable_watch=not args.disable_watch)


================================================
FILE: wheels/README.md
================================================
# How to include Python wheels for addons starting from Blender 4.2

Please read https://docs.blender.org/manual/en/latest/advanced/extensions/python_wheels.html for context.

To bundle Python wheels with your addon, you need to download to this `wheels` directory.
And In blender_manifest.toml include the wheels as a list of paths, e.g.:

```
wheels = [
   "./wheels/pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl",
   "./wheels/pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl",
   "./wheels/pillow-10.3.0-cp311-cp311-win_amd64.whl",
]
```

When you release your addon, the framework will copy these wheels into the zip file according to the toml configuration.
This way you don't have to maintain wheels in your addon directory which could lead to duplication.

Noticed that when testing your addon, the framework will not automatically include these wheels to avoid
overhead. You might see `ModuleNotFoundError` when testing your addon even though you have included the wheels in the
project.
To solve that, you need to install them manually using `pip install` or `python -m pip install` for your testing
Blender python environment. This could also be done in the IDE if you choose to use the same python environment for
developing.

[]:

# 如何给Blender 4.2及以后的插件添加 Python wheels

请参考 https://docs.blender.org/manual/en/latest/advanced/extensions/python_wheels.html 了解背景知识。

给插件添加 Python wheels ,你需要将.whl文件下载到这个 `wheels` 文件夹中.
并且在插件配置文件 blender_manifest.toml 中声明插件所需的whl文件路径,例如:

```
wheels = [
   "./wheels/pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl",
   "./wheels/pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl",
   "./wheels/pillow-10.3.0-cp311-cp311-win_amd64.whl",
]
```

当你打包插件时,框架会根据toml将这些whl文件复制到zip文件中。因此你不需要在插件的目录中存放这些whl文件导致重复。

注意,当你测试插件时,框架不会自动包含这些whl文件以避免额外的开销。你可能会在测试插件时看到 `ModuleNotFoundError`
错误,即使你已经在项目中包含了这些whl文件。
为此你需要手动使用 `pip install` 或 `python -m pip install` 命令
将这些whl文件安装到你进行测试的Blender python环境中。如果你恰巧选择在IDE中使用相同的python环境进行开发,你也可以通过IDE中安装这些whl文件。



================================================
FILE: workflows/addon_release.yml
================================================
# Put this file in the .github/workflows directory of your repository to take effect. Modify the addon name that needs to be automatically released according to your project. (At Step 3)
# Notice you need to change your project setting: Settings -> Actions -> General -> Workflow permissions -> Read and write permission for this workflow to work.
# 将此文件放在您的项目目录 .github/workflows 中即可生效。请根据您的项目需要,修改自动发布的插件名称。(在 Step 3中)
# 注意您需要在Github项目中 Settings -> Actions -> General -> Workflow permissions -> 为此工作流程添加读写权限才能使其正常工作。
name: Automatic Release Addon Action

on:
  push:
    branches:
      - main # Change this to the branch you want to trigger the workflow, e.g., main, master, or develop

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Checkout the code
      - name: Checkout code
        uses: actions/checkout@v3

      # Step 2: Set up Python environment
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      # Step 3: Run the release command for each addon. Please update this step according to your project.
      - name: Run release command for sample_addon
        run: |
          python release.py sample_addon --with_version
# Add additional steps for each addon as needed... 按如下格式添加其他插件的发布命令
#      - name: Run release command for addon1 as extension
#        run: |
#          python release.py addon1 --is_extension --with_version
#
#      - name: Run release command for addon2
#        run: |
#          python release.py addon2 --is_extension --with_version --with_timestamp

      # Add additional steps for each addon as needed...
      # You can add more steps as needed for each addon.

      # Step 4: List files in addon_release directory
      - name: List files in addon_release directory
        run: |
          echo "Files in addon_release directory:"
          ls ../addon_release

      # Step 5: Create GitHub Release
      - name: Create GitHub Release
        id: create_release
        uses: actions/create-release@v1
        with:
          tag_name: "release-${{ github.run_id }}"
          release_name: "Release ${{ github.run_id }}"
          draft: false
          prerelease: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      # Step 6: Upload release assets
      - name: Upload release assets
        run: |
          for file in ../addon_release/*.zip; do
            echo "Uploading $file..."
            gh release upload "release-${{ github.run_id }}" "$file"
          done
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Download .txt
gitextract_qx3brcgf/

├── .gitignore
├── LICENSE
├── README.md
├── addons/
│   ├── __init__.py
│   └── sample_addon/
│       ├── __init__.py
│       ├── blender_manifest.toml
│       ├── config.py
│       ├── i18n/
│       │   ├── __init__.py
│       │   └── dictionary.py
│       ├── operators/
│       │   ├── AddonOperators.py
│       │   └── __init__.py
│       ├── panels/
│       │   ├── AddonPanels.py
│       │   └── __init__.py
│       └── preference/
│           ├── AddonPreferences.py
│           └── __init__.py
├── common/
│   ├── __init__.py
│   ├── class_loader/
│   │   ├── __init__.py
│   │   ├── auto_load.py
│   │   └── module_installer.py
│   ├── i18n/
│   │   ├── __init__.py
│   │   ├── dictionary.py
│   │   └── i18n.py
│   ├── io/
│   │   ├── FileManagerClient.py
│   │   └── __init__.py
│   └── types/
│       ├── __init__.py
│       └── framework.py
├── create.py
├── framework.py
├── main.py
├── release.py
├── requirements.txt
├── test.py
├── wheels/
│   └── README.md
└── workflows/
    └── addon_release.yml
Download .txt
SYMBOL INDEX (89 symbols across 11 files)

FILE: addons/sample_addon/__init__.py
  function register (line 36) | def register():
  function unregister (line 49) | def unregister():

FILE: addons/sample_addon/operators/AddonOperators.py
  class ExampleOperator (line 8) | class ExampleOperator(bpy.types.Operator):
    method poll (line 17) | def poll(cls, context: bpy.types.Context):
    method execute (line 20) | def execute(self, context: bpy.types.Context):

FILE: addons/sample_addon/panels/AddonPanels.py
  class BasePanel (line 9) | class BasePanel(object):
    method poll (line 15) | def poll(cls, context: bpy.types.Context):
  class ExampleAddonPanel (line 20) | class ExampleAddonPanel(BasePanel, bpy.types.Panel):
    method draw (line 24) | def draw(self, context: bpy.types.Context):
    method poll (line 40) | def poll(cls, context: bpy.types.Context):
  class ExampleAddonPanel2 (line 46) | class ExampleAddonPanel2(BasePanel, bpy.types.Panel):
    method draw (line 50) | def draw(self, context: bpy.types.Context):

FILE: addons/sample_addon/preference/AddonPreferences.py
  class ExampleAddonPreferences (line 10) | class ExampleAddonPreferences(AddonPreferences):
    method draw (line 31) | def draw(self, context: bpy.types.Context):

FILE: common/class_loader/auto_load.py
  function init (line 26) | def init():
  function register (line 36) | def register():
  function unregister (line 49) | def unregister():
  function get_all_submodules (line 66) | def get_all_submodules(directory):
  function iter_submodules (line 70) | def iter_submodules(path):
  function iter_submodule_names (line 79) | def iter_submodule_names(path, root=""):
  function get_ordered_classes_to_register (line 92) | def get_ordered_classes_to_register(modules):
  function get_framework_classes (line 96) | def get_framework_classes(modules):
  function get_register_deps_dict (line 105) | def get_register_deps_dict(modules):
  function iter_my_register_deps (line 115) | def iter_my_register_deps(cls, my_classes, my_classes_by_idname):
  function iter_my_deps_from_annotations (line 121) | def iter_my_deps_from_annotations(cls, my_classes):
  function iter_my_deps_from_inheritance (line 133) | def iter_my_deps_from_inheritance(cls, my_classes):
  function get_dependency_from_annotation (line 139) | def get_dependency_from_annotation(value):
  function iter_my_deps_from_parent_id (line 153) | def iter_my_deps_from_parent_id(cls, my_classes_by_idname):
  function iter_my_classes (line 162) | def iter_my_classes(modules):
  function get_classes_in_modules (line 170) | def get_classes_in_modules(modules):
  function iter_classes_in_module (line 178) | def iter_classes_in_module(module):
  function get_register_base_types (line 184) | def get_register_base_types():
  function get_framework_base_classes (line 196) | def get_framework_base_classes():
  function toposort (line 203) | def toposort(deps_dict):
  function register_framework_class (line 242) | def register_framework_class(cls):
  function unregister_framework_class (line 255) | def unregister_framework_class(cls):
  function add_properties (line 262) | def add_properties(property_dict: dict[typing.Any, dict[str, typing.Any]]):
  function remove_properties (line 269) | def remove_properties(property_dict: dict[typing.Any, dict[str, typing.A...

FILE: common/class_loader/module_installer.py
  function install (line 12) | def install(package):
  function has_module (line 16) | def has_module(module_name):
  function is_package_installed (line 23) | def is_package_installed(package_name):
  function install_if_missing (line 31) | def install_if_missing(package):
  function get_blender_version (line 36) | def get_blender_version(blender_exe_path):
  function extract_blender_version (line 60) | def extract_blender_version(blender_exe_path: str):
  function install_fake_bpy (line 70) | def install_fake_bpy(blender_path: str):
  function normalize_blender_path_by_system (line 91) | def normalize_blender_path_by_system(blender_path: str):
  function default_blender_addon_path (line 98) | def default_blender_addon_path(blender_path: str):
  function is_windows (line 115) | def is_windows():
  function is_linux (line 119) | def is_linux():
  function is_mac (line 123) | def is_mac():

FILE: common/i18n/dictionary.py
  function preprocess_dictionary (line 11) | def preprocess_dictionary(dictionary):

FILE: common/i18n/i18n.py
  function set_dictionary (line 24) | def set_dictionary(new_dictionary: dict[str, dict[tuple, str]]):
  function load_dictionary (line 30) | def load_dictionary(additional_dictionary: dict[str, dict[tuple, str]]):
  function i18n (line 41) | def i18n(content: str) -> str:

FILE: common/io/FileManagerClient.py
  function get_all_filename (line 6) | def get_all_filename(folder_path: str) -> list:
  function get_all_subfolder (line 13) | def get_all_subfolder(folder_path: str) -> list:
  function is_subdirectory (line 18) | def is_subdirectory(path_a, path_b) -> bool:
  function is_filename_postfix_in (line 24) | def is_filename_postfix_in(filename: str, target_set: set):
  function search_files (line 34) | def search_files(folder_path: str, post_filter: set) -> list:
  function get_md5 (line 48) | def get_md5(filename):
  function get_md5_folder (line 52) | def get_md5_folder(folder_path: str) -> str:
  function read_utf8 (line 60) | def read_utf8(filepath: str) -> str:
  function read_utf8_in_lines (line 65) | def read_utf8_in_lines(filepath: str) -> list[str]:
  function write_utf8 (line 70) | def write_utf8(filepath: str, content: str):
  function write_utf8_in_lines (line 75) | def write_utf8_in_lines(filepath: str, content: list[str]):

FILE: common/types/framework.py
  function is_extension (line 4) | def is_extension():
  class ExpandableUi (line 11) | class ExpandableUi:
    method draw (line 17) | def draw(self, context: bpy.types.Context):
  function reg_order (line 21) | def reg_order(order_value: int):

FILE: framework.py
  function new_addon (line 46) | def new_addon(addon_name: str):
  function test_addon (line 60) | def test_addon(addon_name, enable_watch=True):
  function get_init_file_path (line 67) | def get_init_file_path(addon_name):
  function start_test (line 121) | def start_test(init_file, addon_name, enable_watch=True):
  function execute_blender_script (line 167) | def execute_blender_script(args, addon_path):
  function read_ext_config (line 182) | def read_ext_config(addon_config_file):
  function release_addon (line 191) | def release_addon(target_init_file, addon_name,
  function get_addon_info (line 328) | def get_addon_info(filename: str):
  function generate_bootstrap_init_file (line 341) | def generate_bootstrap_init_file(addon_name: str, bl_info: dict):
  function remove_pyc_files (line 365) | def remove_pyc_files(release_folder: str):
  function remove_empty_folders (line 371) | def remove_empty_folders(root_path):
  function zip_folder (line 384) | def zip_folder(target_root, output_zip_file, is_extension):
  function find_imported_modules (line 391) | def find_imported_modules(file_path):
  function resolve_module_path (line 411) | def resolve_module_path(module_name, base_path, project_root):
  function find_all_dependencies (line 473) | def find_all_dependencies(file_paths: list, project_root: str):
  function enhance_import_for_py_files (line 523) | def enhance_import_for_py_files(addon_dir: str):
  function convert_absolute_to_relative (line 540) | def convert_absolute_to_relative(file_path: str, project_root: str):
  function find_all_py_modules (line 607) | def find_all_py_modules(root_dir: str) -> set:
  function start_watch_for_update (line 623) | def start_watch_for_update(init_file, addon_name, stop_event: threading....
  function update_addon_for_test (line 666) | def update_addon_for_test(init_file, addon_name):
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (101K chars).
[
  {
    "path": ".gitignore",
    "chars": 3079,
    "preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
  },
  {
    "path": "LICENSE",
    "chars": 1066,
    "preview": "MIT License\n\nCopyright (c) 2024 ZHU Xinyu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "README.md",
    "chars": 14698,
    "preview": "# Blender Add-on Development Framework and Packaging Tool\n\n### Demo 1: Auto-update while developing 开发过程支持自动更新\n\n![Demo1]"
  },
  {
    "path": "addons/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "addons/sample_addon/__init__.py",
    "chars": 1672,
    "preview": "import bpy\n\nfrom .config import __addon_name__\nfrom .i18n.dictionary import dictionary\nfrom ...common.class_loader impor"
  },
  {
    "path": "addons/sample_addon/blender_manifest.toml",
    "chars": 2804,
    "preview": "schema_version = \"1.0.0\"\n\n# Example of manifest file for a Blender extension\n# Change the values according to your exten"
  },
  {
    "path": "addons/sample_addon/config.py",
    "chars": 475,
    "preview": "from ...common.types.framework import is_extension\n\n# https://docs.blender.org/manual/en/latest/advanced/extensions/addo"
  },
  {
    "path": "addons/sample_addon/i18n/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "addons/sample_addon/i18n/dictionary.py",
    "chars": 692,
    "preview": "from common.i18n.dictionary import preprocess_dictionary\n\ndictionary = {\n    \"zh_CN\": {\n        (\"*\", \"Example Addon Sid"
  },
  {
    "path": "addons/sample_addon/operators/AddonOperators.py",
    "chars": 892,
    "preview": "import bpy\n\nfrom ..config import __addon_name__\nfrom ..preference.AddonPreferences import ExampleAddonPreferences\n\n\n# Th"
  },
  {
    "path": "addons/sample_addon/operators/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "addons/sample_addon/panels/AddonPanels.py",
    "chars": 1508,
    "preview": "import bpy\n\nfrom ..config import __addon_name__\nfrom ..operators.AddonOperators import ExampleOperator\nfrom ....common.i"
  },
  {
    "path": "addons/sample_addon/panels/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "addons/sample_addon/preference/AddonPreferences.py",
    "chars": 1186,
    "preview": "import os\n\nimport bpy\nfrom bpy.props import StringProperty, IntProperty, BoolProperty\nfrom bpy.types import AddonPrefere"
  },
  {
    "path": "addons/sample_addon/preference/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/class_loader/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/class_loader/auto_load.py",
    "chars": 8447,
    "preview": "import importlib\nimport inspect\nimport pkgutil\nimport typing\nfrom pathlib import Path\n\nimport bpy\n\n__all__ = (\n    \"init"
  },
  {
    "path": "common/class_loader/module_installer.py",
    "chars": 4448,
    "preview": "# Notice: Please do not use functions in this file for developing your Blender Addons, this file is for internal use of\n"
  },
  {
    "path": "common/i18n/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/i18n/dictionary.py",
    "chars": 730,
    "preview": "common_dictionary = {\n    \"zh_CN\": {\n        # (\"*\", \"translation\"): \"翻译\",\n    }\n}\n\ncommon_dictionary[\"zh_HANS\"] = commo"
  },
  {
    "path": "common/i18n/i18n.py",
    "chars": 1683,
    "preview": "import bpy\n\n# Get the language code when addon start up\n__language_code__ = bpy.context.preferences.view.language\n\nfrom "
  },
  {
    "path": "common/io/FileManagerClient.py",
    "chars": 2462,
    "preview": "import hashlib\nimport os\nfrom os import listdir\n\n\ndef get_all_filename(folder_path: str) -> list:\n    if os.path.exists("
  },
  {
    "path": "common/io/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/types/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "common/types/framework.py",
    "chars": 1465,
    "preview": "import bpy\n\n\ndef is_extension():\n    # Blender extension package starts with \"bl_ext.\"\n    # https://docs.blender.org/ma"
  },
  {
    "path": "create.py",
    "chars": 455,
    "preview": "from framework import new_addon\nfrom main import ACTIVE_ADDON\n\n# 创建前请修改以下参数\n\n# The name of the addon to be created, this"
  },
  {
    "path": "framework.py",
    "chars": 28552,
    "preview": "import ast\nimport atexit\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport threading\nimport time\nfro"
  },
  {
    "path": "main.py",
    "chars": 4249,
    "preview": "# BlenderAddonPackageTool - A framework for developing multiple blender addons in a single workspace\n# Copyright (C) 202"
  },
  {
    "path": "release.py",
    "chars": 2886,
    "preview": "from framework import get_init_file_path, release_addon\nfrom main import ACTIVE_ADDON, IS_EXTENSION\n\n# 发布前请修改ACTIVE_ADDO"
  },
  {
    "path": "test.py",
    "chars": 715,
    "preview": "from framework import test_addon\nfrom main import ACTIVE_ADDON\n\n# 测试前请修改ACTIVE_ADDON参数\n\n# The name of the addon to be te"
  },
  {
    "path": "wheels/README.md",
    "chars": 1993,
    "preview": "# How to include Python wheels for addons starting from Blender 4.2\n\nPlease read https://docs.blender.org/manual/en/late"
  },
  {
    "path": "workflows/addon_release.yml",
    "chars": 2605,
    "preview": "# Put this file in the .github/workflows directory of your repository to take effect. Modify the addon name that needs t"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the xzhuah/BlenderAddonPackageTool GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (86.7 KB), approximately 22.5k tokens, and a symbol index with 89 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!