diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9115925 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +*.pyc diff --git a/entry_point.py b/entry_point.py new file mode 100644 index 0000000..6deefff --- /dev/null +++ b/entry_point.py @@ -0,0 +1,11 @@ +import importlib +import os +import sys + +script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, script_dir) +__package__ = os.path.basename(script_dir) + +from .template import __init__ + +__init__.test() \ No newline at end of file diff --git a/hello-world.zip b/hello-world.zip index 7ed75fd..06c5d84 100644 Binary files a/hello-world.zip and b/hello-world.zip differ diff --git a/hello-world/__init__.py b/hello-world/__init__.py index 4fba2b4..163dad0 100644 --- a/hello-world/__init__.py +++ b/hello-world/__init__.py @@ -1,4 +1,7 @@ +from . import util + def register(): print("Hello World") + util.extra_print() def unregister(): print("Goodbye World") \ No newline at end of file diff --git a/hello-world/blender_manifest.toml b/hello-world/blender_manifest.toml index fb63b75..258fd50 100644 --- a/hello-world/blender_manifest.toml +++ b/hello-world/blender_manifest.toml @@ -10,6 +10,7 @@ maintainer = "Simon Nordon " # Supported types: "add-on", "theme" type = "add-on" +# Optional link to documentation, support, source files, etc # Optional link to documentation, support, source files, etc # website = "https://extensions.blender.org/add-ons/my-example-package/" diff --git a/hello-world/util.py b/hello-world/util.py new file mode 100644 index 0000000..7581498 --- /dev/null +++ b/hello-world/util.py @@ -0,0 +1,2 @@ +def extra_print(): + print("Extra print from hello-world/util.py") \ No newline at end of file diff --git a/index.html b/index.html index c12fc77..8783343 100644 --- a/index.html +++ b/index.html @@ -25,10 +25,10 @@ ~ 4.2.0 - ~ all - 1.6KB + 1.8KB -

Built 2024-08-05, 05:37

+

Built 2024-08-05, 10:17

diff --git a/index.json b/index.json index c7f1cef..d0d4e44 100644 --- a/index.json +++ b/index.json @@ -19,8 +19,8 @@ "Sequencer" ], "archive_url": "./hello-world.zip", - "archive_size": 1596, - "archive_hash": "sha256:99f4c18b16b81d392894c9bc75a020e2e90e43dda15cd3809d8a03f588148ac4" + "archive_size": 1805, + "archive_hash": "sha256:9deb8200f0a086a7029f3eb46db0ae6b40cb27347a3b37f1337ccd06d4b06e6e" } ] } \ No newline at end of file diff --git a/readme.md b/readme.md index d2b48e3..6fb763e 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,16 @@ -https://docs.blender.org/manual/en/latest/advanced/extensions/creating_repository/static_repository.html +https://simonnordon4.github.io/blender-extensions/index.json + + + +blender --command extension server-generate --repo-dir=E:\repos\blender-extensions\ --html + +Compress-Archive -Path ".\hello-world" -DestinationPath "hello-world.zip" -Force + +1. Create a new directory for the extension. +2. Compres the directory into a zip file. +3. Run blender extension command. +4. Upload the zip file to the server. +5. Ensure repo is public and a website page. + +References: + https://docs.blender.org/manual/en/latest/advanced/extensions/creating_repository/static_repository.html \ No newline at end of file diff --git a/template/__init__.py b/template/__init__.py new file mode 100644 index 0000000..bf323d3 --- /dev/null +++ b/template/__init__.py @@ -0,0 +1,14 @@ +# import auto_load + +# auto_load.init() + +# def register(): +# auto_load.register() + +# def unregister(): +# auto_load.unregister() + +print("Hello from __init__.py") + +def test(): + print("Hello from __init__.py test()") \ No newline at end of file diff --git a/template/auto_load.py b/template/auto_load.py new file mode 100644 index 0000000..16da589 --- /dev/null +++ b/template/auto_load.py @@ -0,0 +1,152 @@ +import os +import bpy +import sys +import typing +import inspect +import pkgutil +import importlib +from pathlib import Path + +blender_version = bpy.app.version + +modules = None +ordered_classes = None + +def init(): + global modules, ordered_classes + modules = get_all_submodules(Path(__file__).parent) + ordered_classes = get_ordered_classes_to_register(modules) + + +def register(): + # Check if class is already registered before registering it + for cls in ordered_classes: + try: + bpy.utils.register_class(cls) + except: + print("Failed to register", cls) + + for module in modules: + if module.__name__ == __name__: + continue + if hasattr(module, "register"): + module.register() + +def unregister(): + if ordered_classes is None: + return + 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() + +def get_all_submodules(directory): + return list(iter_submodules(directory, directory.name)) + +def iter_submodules(path, package_name): + for name in sorted(iter_submodule_names(path)): + module = importlib.import_module(name) + try: + importlib.reload(module) + except: + pass + yield module + +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 + +def get_ordered_classes_to_register(modules): + return toposort(get_register_deps_dict(modules)) + +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_parent_id(cls, my_classes_by_idname) + +def iter_my_deps_from_annotations(cls, my_classes): + for value in typing.get_type_hints(cls, {}, {}).values(): + dependency = get_dependency_from_annotation(value) + if dependency is not None: + if dependency in my_classes: + yield dependency + +def get_dependency_from_annotation(value): + if blender_version >= (2, 93): + if isinstance(value, bpy.props._PropertyDeferred): + 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.__bases__: + 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.__bases__): + if not getattr(cls, "is_registered", False): + yield cls + else: + bpy.utils.unregister_class(cls) + 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(): + return set(getattr(bpy.types, name) for name in [ + "Panel", "Operator", "PropertyGroup", + "AddonPreferences", "Header", "Menu", + "Node", "NodeSocket", "NodeTree", + "UIList", "RenderEngine", + "Gizmo", "GizmoGroup", + ]) + +def toposort(deps_dict): + sorted_list = [] + sorted_values = set() + while len(deps_dict) > 0: + unsorted = [] + for value, deps in deps_dict.items(): + if len(deps) == 0: + sorted_list.append(value) + sorted_values.add(value) + else: + unsorted.append(value) + deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted} + return sorted_list \ No newline at end of file diff --git a/template/blender_manifest.toml b/template/blender_manifest.toml new file mode 100644 index 0000000..70e2f54 --- /dev/null +++ b/template/blender_manifest.toml @@ -0,0 +1,74 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "template" +version = "1.0.0" +name = "Template" +tagline = "Template for a Blender Addon" +maintainer = "Simon Nordon " +# Supported types: "add-on", "theme" +type = "add-on" + +# Optional link to documentation, support, source files, etc +# 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 = ["Object"] + +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-2.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", +# ] \ No newline at end of file diff --git a/template/template_dev_panel.py b/template/template_dev_panel.py new file mode 100644 index 0000000..bad361f --- /dev/null +++ b/template/template_dev_panel.py @@ -0,0 +1,33 @@ +import bpy +import auto_load + +class TemplateDevPanel(bpy.types.Panel): + bl_idname = "DEV_PT_template_dev_panel" + bl_label = "Template Dev Panel" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Template" + + def draw(self, context): + layout = self.layout + layout.label(text="Template Panel", icon="MODIFIER") + layout.operator("dev_panel.reload_addon") + layout.operator("dev_panel.uninstall_addon") + +class ReloadAddonOperator(bpy.types.Operator): + bl_idname = "dev_panel.reload_addon" + bl_label = "Reload Addon" + + def execute(self, context): + auto_load.unregister() + auto_load.init() + auto_load.register() + return {"FINISHED"} + +class UninstallAddonOperator(bpy.types.Operator): + bl_idname = "dev_panel.uninstall_addon" + bl_label = "Uninstall Addon" + + def execute(self, context): + auto_load.unregister() + return {"FINISHED"}