diff --git a/.github/workflows/upload-assets.yml b/.github/workflows/upload-assets.yml index 7c6898b..02db20a 100644 --- a/.github/workflows/upload-assets.yml +++ b/.github/workflows/upload-assets.yml @@ -22,7 +22,7 @@ jobs: # id: draft_release # uses: cardinalby/git-get-release-action@1.2.4 # with: - # releaseName: Draft + # releaseName: Upload # env: # GITHUB_TOKEN: ${{ github.token }} @@ -70,5 +70,38 @@ jobs: with: upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps asset_path: ./package.zip - asset_name: BioxelNodes_${{ needs.draft_release.outputs.version }}.zip + asset_name: BioxelNodes_Addon_${{ needs.draft_release.outputs.version }}.zip + asset_content_type: application/zip + + upload_blender_extension: + name: Upload Blender Extension + needs: draft_release + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + lfs: 'true' + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Zip Extension + run: | + pip download SimpleITK --dest bioxelnodes/wheels --only-binary=:all: --python-version=3.11 --platform=macosx_11_0_arm64 + pip download SimpleITK --dest bioxelnodes/wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64 + rm -r bioxelnodes/externalpackage + cp extension/__init__.py bioxelnodes/__init__.py + cp extension/preferences.py bioxelnodes/preferences.py + cp extension/blender_manifest.toml bioxelnodes/blender_manifest.toml + zip -r package.zip bioxelnodes + - name: Upload Release Asset + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps + asset_path: ./package.zip + asset_name: BioxelNodes_Extension_${{ needs.draft_release.outputs.version }}.zip asset_content_type: application/zip diff --git a/README.md b/README.md index 4920708..ddb4eb9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Before us, there have been many tutorials and add-ons for importing volumetric d Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail: -![gallery](docs/assets/gallery.png) +![cover](docs/assets/cover.png) The "Bioxel" in "Bioxel Nodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. We are developing a toolkit around Bioxel for better biological data visualization. but before its release, we made this Blender version of bioxels toolkit first, in order to let more people to have fun with volumetric data. [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started) diff --git a/bioxelnodes/auto_load.py b/bioxelnodes/auto_load.py index 6b4eede..145371c 100644 --- a/bioxelnodes/auto_load.py +++ b/bioxelnodes/auto_load.py @@ -57,7 +57,7 @@ def unregister(): ################################################# def get_all_submodules(directory): - return list(iter_submodules(directory, directory.name)) + return list(iter_submodules(directory, __package__)) def iter_submodules(path, package_name): diff --git a/bioxelnodes/externalpackage/package.py b/bioxelnodes/externalpackage/package.py index 368a32c..d0faf59 100644 --- a/bioxelnodes/externalpackage/package.py +++ b/bioxelnodes/externalpackage/package.py @@ -76,7 +76,8 @@ def start_logging(self, logfile_name: str = 'side-packages-install') -> logging. # Set up logging configuration logfile_path = Path(self.log_path, f"{logfile_name}.log") - logging.basicConfig(filename=logfile_path, level=logging.INFO) + logging.basicConfig(filename=logfile_path, + level=logging.INFO, encoding='utf-8') # Return logger object return logging.getLogger() @@ -248,13 +249,16 @@ def run_python(self, cmd_list: list = None, timeout: int = 600): result = subprocess.run(cmd_list, timeout=timeout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - if result.returncode != 0: - log.error('Command failed: %s', cmd_list) - log.error('stdout: %s', result.stdout.decode()) - log.error('stderr: %s', result.stderr.decode(errors='ignore')) - else: - log.info('Command succeeded: %s', cmd_list) - log.info('stdout: %s', result.stdout.decode()) + try: + if result.returncode != 0: + log.error('Command failed: %s', cmd_list) + log.error('stdout: %s', result.stdout.decode()) + log.error('stderr: %s', result.stderr.decode(errors='ignore')) + else: + log.info('Command succeeded: %s', cmd_list) + log.info('stdout: %s', result.stdout.decode()) + except: + ... # return the command list, return code, stdout, and stderr as a tuple return result diff --git a/bioxelnodes/io_points.py b/bioxelnodes/io_points.py index 083fdf8..0bd36f7 100644 --- a/bioxelnodes/io_points.py +++ b/bioxelnodes/io_points.py @@ -22,200 +22,4 @@ def create_points_obj(points, name="points"): # make the bmesh the object's mesh bm.to_mesh(mesh) bm.free() # always do this when finished - return obj - - -# class ImportDICOMPointsDialog(bpy.types.Operator): -# bl_idname = "bioxelnodes.import_dicom_points_dialog" -# bl_label = "Volume Data as Bioxels" -# bl_description = "Import Volume Data as Bioxels (VDB)" -# bl_options = {'UNDO'} - -# filepath: bpy.props.StringProperty( -# subtype="FILE_PATH", -# options={'HIDDEN'} -# ) # type: ignore - -# bioxels_shape: bpy.props.IntVectorProperty( -# name="Bioxels Shape (ReadOnly)", -# min=0, -# default=(100, 100, 100) -# ) # type: ignore - -# bioxel_size: bpy.props.FloatProperty( -# name="Bioxel Size", -# soft_min=0.1, soft_max=10.0, -# min=1e-2, max=1e2, -# default=1, -# update=on_bioxel_size_changed -# ) # type: ignore - -# orig_spacing: bpy.props.FloatVectorProperty( -# name="Original Spacing", -# default=(1, 1, 1), -# update=on_orig_spacing_changed -# ) # type: ignore - -# orig_shape: bpy.props.IntVectorProperty( -# name="Original Shape", -# default=(100, 100, 100), -# options={'HIDDEN'} -# ) # type: ignore - -# auto: bpy.props.BoolProperty( -# name="Auto Setting", -# default=False, -# options={'HIDDEN'} -# ) # type: ignore - -# scene_scale: bpy.props.FloatProperty( -# name="Scene Scale", -# soft_min=0.001, soft_max=100.0, -# min=1e-6, max=1e6, -# default=0.01, -# ) # type: ignore - -# do_add_segmentnode: bpy.props.BoolProperty( -# name="Add Segment Node", -# default=True, -# ) # type: ignore - -# do_change_render_setting: bpy.props.BoolProperty( -# name="Change Render Setting", -# default=True, -# ) # type: ignore - -# def execute(self, context): -# files = get_data_files(self.filepath) -# name = Path(self.filepath).parent.name - -# image = sitk.ReadImage(files) - -# bioxel_size = float(self.bioxel_size) -# orig_spacing = tuple(self.orig_spacing) -# image_spacing = image.GetSpacing() -# image_shape = image.GetSize() - -# bioxels_spacing = ( -# image_spacing[0] / orig_spacing[0] * bioxel_size, -# image_spacing[1] / orig_spacing[1] * bioxel_size, -# image_spacing[2] / orig_spacing[2] * bioxel_size -# ) - -# bioxels_shape = ( -# int(image_shape[0] / bioxel_size * orig_spacing[0]), -# int(image_shape[1] / bioxel_size * orig_spacing[1]), -# int(image_shape[2] / bioxel_size * orig_spacing[2]), -# ) - -# print("Resampling...") -# image = sitk.Resample( -# image1=image, -# size=bioxels_shape, -# transform=sitk.Transform(), -# interpolator=sitk.sitkLinear, -# outputOrigin=image.GetOrigin(), -# outputSpacing=bioxels_spacing, -# outputDirection=image.GetDirection(), -# defaultPixelValue=0, -# outputPixelType=image.GetPixelID(), -# ) - -# print("Orienting to RAS...") -# image = sitk.DICOMOrient(image, 'RAS') - -# array = sitk.GetArrayFromImage(image) -# orig_dtype = str(array.dtype) -# print(f"Coverting Dtype from {orig_dtype} to float...") -# array = array.astype(float) - -# # ITK indices, by convention, are [i,j,k] while NumPy indices are [k,j,i] -# # https://www.slicer.org/wiki/Coordinate_systems - -# # ITK Numpy 3D -# # R (ight) i -> k -> x -# # A (nterior) j -> j -> y -# # S (uperior) k -> i -> z - -# array = np.transpose(array) -# bioxels_max = float(np.max(array)) -# bioxels_min = float(np.min(array)) -# bioxels_shape = array.shape - -# print("Bioxel Size:", bioxel_size) -# print("Bioxels Shape:", bioxels_shape) - -# bioxels_offset = 0.0 -# if bioxels_min < 0 and orig_dtype[0] != "u": -# bioxels_offset = -bioxels_min -# array = array + np.full_like(array, bioxels_offset) -# bioxels_max = float(np.max(array)) -# bioxels_min = float(np.min(array)) -# print("Offseted Max:", bioxels_max) -# print("Offseted Min:", bioxels_min) - -# points = [] -# for x in range(array.shape[0]): -# for y in range(array.shape[1]): -# for z in range(array.shape[2]): -# points.append({ -# "pos": (x, y, z), -# "value": array[x, y, z], -# }) - -# # # Build VDB -# # grid = vdb.FloatGrid() -# # grid.copyFromArray(array.copy()) - -# # # After sitk.DICOMOrient(), origin and direction will also orient base on LPS -# # # so we need to convert them into RAS -# # mat_lps2ras = axis_conversion( -# # from_forward='-Z', -# # from_up='-Y', -# # to_forward='-Z', -# # to_up='Y' -# # ).to_4x4() - -# # mat_location = mathutils.Matrix.Translation( -# # mathutils.Vector(image.GetOrigin()) -# # ) - -# # mat_rotation = mathutils.Matrix( -# # np.array(image.GetDirection()).reshape((3, 3)) -# # ).to_4x4() - -# # mat_scale = mathutils.Matrix.Scale( -# # bioxel_size, 4 -# # ) - -# # transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale - -# obj = create_points_obj(points) - -# # Make transformation -# scene_scale = float(self.scene_scale) -# # (S)uperior -Z -> Y -# # (A)osterior Y -> Z -# mat_ras2blender = axis_conversion( -# from_forward='-Z', -# from_up='Y', -# to_forward='Y', -# to_up='Z' -# ).to_4x4() - -# mat_scene_scale = mathutils.Matrix.Scale( -# scene_scale, 4 -# ) - -# obj.matrix_world = mat_ras2blender @ mat_scene_scale - -# self.report({"INFO"}, "Successfully Imported") - -# return {'FINISHED'} - -# def invoke(self, context, event): -# # print(tuple(self.orig_shape)) -# self.auto = True -# self.bioxel_size = 1 -# context.window_manager.invoke_props_dialog(self) -# return {'RUNNING_MODAL'} + return obj \ No newline at end of file diff --git a/bioxelnodes/misc.py b/bioxelnodes/misc.py index 9d3d1db..bbc8fba 100644 --- a/bioxelnodes/misc.py +++ b/bioxelnodes/misc.py @@ -1,7 +1,7 @@ import bpy from pathlib import Path import shutil -from bioxelnodes.utils import get_all_layers, get_container, get_container_layers +from .utils import get_all_layers, get_container, get_container_layers def save_layer(layer, output_dir): diff --git a/bioxelnodes/nodes.py b/bioxelnodes/nodes.py index 0a95ec0..af6633e 100644 --- a/bioxelnodes/nodes.py +++ b/bioxelnodes/nodes.py @@ -7,7 +7,7 @@ def add_driver_to_node_factory(source_prop, target_prop): callback_str = f""" import bpy -from bioxelnodes.utils import add_direct_driver, get_bioxels_obj +from .utils import add_direct_driver, get_bioxels_obj bioxels_obj = get_bioxels_obj(bpy.context.active_object) if bioxels_obj: container_obj = bioxels_obj.parent @@ -26,7 +26,7 @@ def add_driver_to_node_factory(source_prop, target_prop): def set_prop_to_node_factory(source_prop, target_prop): callback_str = f""" import bpy -from bioxelnodes.utils import get_bioxels_obj +from .utils import get_bioxels_obj bioxels_obj = get_bioxels_obj(bpy.context.active_object) if bioxels_obj: container_obj = bioxels_obj.parent diff --git a/bioxelnodes/preferences.py b/bioxelnodes/preferences.py index e239a0c..24a44b0 100644 --- a/bioxelnodes/preferences.py +++ b/bioxelnodes/preferences.py @@ -26,5 +26,4 @@ def draw(self, context): layout.label(text="Configuration") layout.prop(self, 'cache_dir') - layout.prop(self, "do_change_render_setting") diff --git a/docs/assets/cover.png b/docs/assets/cover.png new file mode 100644 index 0000000..3d99b2e Binary files /dev/null and b/docs/assets/cover.png differ diff --git a/docs/getting-started.md b/docs/getting-started.md index cc0a271..5f2f4a4 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,8 +4,19 @@ Currently only support Blender 4.0 or above, make sure you have the correct vers ## Add-on Installation -Download the latest version https://github.com/OmooLab/BioxelNodes/releases/latest -In Blender, Edit > Preferences > Add-ons > Install, select the `BioxelNodes_{version}.zip` you just downloaded. +#### For Blender 4.2 or higher + +Download the **Extension** version `BioxelNodes_Extension_{version}.zip` from https://github.com/OmooLab/BioxelNodes/releases/latest +In Blender, Edit > Preferences > Extensions > Install from Disk, select the zip file you just downloaded. + +Thats it! + +> If it cannot be enable, just reboot blender. + +#### For Blender 4.0 or 4.1 + +Download the **Addon** version `BioxelNodes_Addon_{version}.zip` from https://github.com/OmooLab/BioxelNodes/releases/latest +In Blender, Edit > Preferences > Add-ons > Install, select the zip file you just downloaded. The add-on requires a third-party python dependency called SimpleITK, click `Install SimpleITK` button below to install the dependency. After clicking, blender may get stuck, it is downloading and installing, just wait for a moment. After that, click `Reboot Blender` button. diff --git a/docs/index.md b/docs/index.md index a54bfff..8b15d62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ Before us, there have been many tutorials and add-ons for importing volumetric d Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail: -![gallery](assets/gallery.png) +![cover](assets/cover.png) The "Bioxel" in "Bioxel Nodes", is a combination of the words "Bio-" and "Voxel". Bioxel is a voxel that stores biological data. We are developing a toolkit around Bioxel for better biological data visualization. but before its release, we made this Blender version of bioxels toolkit first, in order to let more people to have fun with volumetric data. [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started) diff --git a/extension/__init__.py b/extension/__init__.py new file mode 100644 index 0000000..9d122ed --- /dev/null +++ b/extension/__init__.py @@ -0,0 +1,22 @@ +import bpy + +from . import auto_load +from . import menus + + +auto_load.init() + + +def register(): + auto_load.register() + menus.add() + bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty( + name="Bioxel Layers Directory", + subtype='DIR_PATH', + default="//" + ) + + +def unregister(): + menus.remove() + auto_load.unregister() diff --git a/extension/blender_manifest.toml b/extension/blender_manifest.toml new file mode 100644 index 0000000..9108043 --- /dev/null +++ b/extension/blender_manifest.toml @@ -0,0 +1,56 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "bioxelnodes" +version = "0.2.0" +name = "Bioxel Nodes" +tagline = "For scientific volumetric data visualization in Blender" +maintainer = "Ma Nan " +# Supported types: "add-on", "theme" +type = "add-on" + +# 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) +permissions = ["files"] + +# Optional link to documentation, support, source files, etc +website = "https://omoolab.github.io/BioxelNodes/latest" + +# Optional list defined by Blender and server, see: +# https://docs.blender.org/manual/en/dev/extensions/tags.html +tags = ["Geometry Nodes", "Render", "Material"] + +blender_version_min = "4.2.0" +# Optional: maximum supported Blender version +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/extensions/licenses.html +license = ["SPDX:MIT"] +# Optional: required by some licenses. +copyright = ["2024 OmooLab"] + +# Optional list of supported platforms. If omitted, the extension will be available in all operating systems. +platforms = ["windows-amd64", "macos-x86_64"] +# Other supported platforms: "windows-arm64", "macos-x86_64" + +# TODO: externalpackage to wheels +# Optional: bundle 3rd party Python modules. +# https://docs.blender.org/manual/en/dev/extensions/python_wheels.html +wheels = [ + "./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", + "./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", +] + +# Optional: build setting. +# https://docs.blender.org/manual/en/dev/extensions/command_line_arguments.html#command-line-args-extension-build +# [build] +# paths_exclude_pattern = [ +# "/.git/" +# "__pycache__/" +# ] diff --git a/extension/preferences.py b/extension/preferences.py new file mode 100644 index 0000000..754a64d --- /dev/null +++ b/extension/preferences.py @@ -0,0 +1,23 @@ +import bpy +from pathlib import Path + +class BioxelNodesPreferences(bpy.types.AddonPreferences): + bl_idname = __package__ + + cache_dir: bpy.props.StringProperty( + name="Set Cache Directory", + subtype='DIR_PATH', + default=str(Path(Path.home(), '.bioxelnodes')) + ) # type: ignore + + do_change_render_setting: bpy.props.BoolProperty( + name="Change Render Setting", + default=True, + ) # type: ignore + + def draw(self, context): + layout = self.layout + + layout.label(text="Configuration") + layout.prop(self, 'cache_dir') + layout.prop(self, "do_change_render_setting") diff --git a/mkdocs.yml b/mkdocs.yml index 7e89023..24e9ad3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,11 +8,11 @@ repo_url: https://github.com/OmooLab/BioxelNodes nav: - BioxelNodes: - - index.md - - Getting Started: getting-started.md - - Features & Options: features.md - - Nodes: nodes.md - - Future Features: misc.md + - index.md + - Getting Started: getting-started.md + - Features & Options: features.md + - Nodes: nodes.md + - Future Features: misc.md theme: name: material @@ -57,6 +57,7 @@ theme: extra: version: provider: mike + default: latest extra_css: - stylesheets/extra.css