From 99ce5c28d07d2b5e745de7b939b0000e1aeacfd5 Mon Sep 17 00:00:00 2001 From: Ma Nan Date: Tue, 9 Jul 2024 15:20:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20dramatic=20reduction=20in=20rendering?= =?UTF-8?q?=20time=20=F0=9F=9A=80=20feat:=20add=20more=20support=20to=20?= =?UTF-8?q?=20eevee=20render=20fix:=20fail=20to=20add=20utils=20node=20inf?= =?UTF-8?q?late=20fix:=20file=20import=20button=20missing=20chore:=20chang?= =?UTF-8?q?e=20operator=20names=20BREAKING=20CHANGE:=20remove=20support=20?= =?UTF-8?q?to=20blender=204.0=20BREAKING=20CHANGE:=20remove=20support=20to?= =?UTF-8?q?=20gpu=20render=20with=20cuda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +- bioxelnodes/__init__.py | 9 +- .../assets/Nodes/BioxelNodes_4.0.blend | 3 - .../assets/Nodes/BioxelNodes_4.1.blend | 4 +- bioxelnodes/customnodes/menus.py | 63 +-- bioxelnodes/customnodes/nodes.py | 9 +- bioxelnodes/io.py | 120 ++-- bioxelnodes/menus.py | 42 +- bioxelnodes/misc.py | 92 ---- bioxelnodes/nodes.py | 520 +++++++----------- bioxelnodes/operators.py | 108 ++-- bioxelnodes/save.py | 165 ++++++ bioxelnodes/utils.py | 28 +- extension/__init__.py | 5 - extension/blender_manifest.toml | 2 +- pyproject.toml | 2 +- 16 files changed, 577 insertions(+), 624 deletions(-) delete mode 100644 bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend delete mode 100644 bioxelnodes/misc.py create mode 100644 bioxelnodes/save.py diff --git a/README.md b/README.md index af32c89..72ab125 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ ![GitHub Release](https://img.shields.io/github/v/release/OmooLab/BioxelNodes?style=for-the-badge) ![GitHub Repo stars](https://img.shields.io/github/stars/OmooLab/BioxelNodes?style=for-the-badge) -Bioxel Nodes is a Blender extension for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes and Cycles to process and render volumetric data. You are free to share your blender file to anyone who does not install this extension, since most processes were done by Blender's native nodes. +Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It using Blender's powerful Geometry Nodes and Cycles to process and render volumetric data. You are free to share your blender file to anyone who does not install this extension, since most processes were done by Blender's native nodes. ## About -Before us, there have been many tutorials and extensions for importing volumetric data into Blender. However, we found that there were many scientific issues that were not addressed in place, and the volume render results were not epic. With Bioxel Nodes, you can easily import any format volumetric data into Blender, and more importantly, make a beautiful realistic volume rendering quickly. +Before us, there have been many tutorials and addons for importing volumetric data into Blender. However, we found that there were many scientific issues that were not addressed in place, and the volume render results were not epic. With Bioxel Nodes, you can easily import any format volumetric data into Blender, and more importantly, make a beautiful realistic volume rendering quickly. Below are some examples with Bioxel Nodes. Thanks to Cycles Render, the volumetric data can be rendered with great detail: @@ -41,30 +41,29 @@ So how to use this extension? please check [Getting Started](https://omoolab.git ## Known Limitations +- Cycles CPUs only, Cycles GPUs (Optix), EEVEE (partial support) - Sections cannot be generated (will be supported soon) - Time sequence volume not supported (will be supported soon) -## Upgrade from 0.1.x to 0.2.x +## Compatibility to Newer Version -You need to do the following: +Addon are updating, and it is possible that newer versions of the addon will not work on old project files properly. In order to make the old files work, you can do the following: -1. Remove the old version of Bioxel Nodes at Preferences > Add-ons -2. Add the new version and restart Blender. +### For project files that need to be archived -It is not support editing the same blender file across extension versions. In order to make sure that the previous file works properly. You need to save the staged data before upgrading ( read the last section of [Getting Started](https://omoolab.github.io/BioxelNodes/latest/getting-started/#share-your-file) ). +Persistently save the Addon nodes before the addon is updated. This will put the nodes out of sync with the addon functionality, but it will ensure that the entire file can be calculated and rendered correctly, it fit to project files that you need to archive. +Bioxel Nodes > Save All Staged Data -But even then, there is still no guarantee that the new version of the extension will work on the old blender file. Therefore, it is highly recommended to open a new blender file to start the creating, not based on the old one. +### For working project files -Alternatively, objects from the old file that have nothing to do with Bioxel Nodes could be append to the new blender file. +If you've ever done a persistent save of Bioxel Nodes nodes, it's possible that after the addon update, there may be new features of the addon that don't synergize with the saved nodes. In order for the new version to work with the old nodes, you need to relink them. +Bioxel Nodes > Relink Nodes to Addon ## About EEVEE Render -Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially. "Solid Shader" node and "Volume Shader" node have a toggle called "EEVEE Render". If you want to render Bioxel Component in real-time, turn it on. - -Also, there are some limitations: +Bioxel Nodes is designed for Cycles Render. However, it does support eevee render partially.Also, there are some limitations: 1. Only one cutter supported. -2. You cannot use "Color Ramp" over 2 colors. -3. EEVEE Render result is not that great as Cycles does. +2. EEVEE Render result is not that great as Cycles does. -> "Volume Shader" node is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But the Blender 4.2 docs still say attributes reading is ok, so I suppose this feature will eventually be implemented. +> Volume Shader is not work properly in EEVEE Next since 4.2. It is because EEVEE Next is not support attributes from instances of volume shader by now. But Blender 4.3 is ok, so I suppose this issue will eventually be fixed. diff --git a/bioxelnodes/__init__.py b/bioxelnodes/__init__.py index e02f99f..766101a 100644 --- a/bioxelnodes/__init__.py +++ b/bioxelnodes/__init__.py @@ -8,8 +8,8 @@ "name": "Bioxel Nodes", "author": "Ma Nan", "description": "", - "blender": (4, 0, 0), - "version": (0, 2, 4), + "blender": (4, 1, 0), + "version": (0, 2, 5), "location": "File -> Import", "warning": "", "category": "Node" @@ -21,11 +21,6 @@ def register(): auto_load.register() menus.add() - bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty( - name="Layer Directory", - subtype='DIR_PATH', - default="//" - ) def unregister(): diff --git a/bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend b/bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend deleted file mode 100644 index eb31331..0000000 --- a/bioxelnodes/assets/Nodes/BioxelNodes_4.0.blend +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6721aaf62fda8d104312d290032ab045b9f7d06f7f71ef9dd17b851c0e7293b5 -size 5410056 diff --git a/bioxelnodes/assets/Nodes/BioxelNodes_4.1.blend b/bioxelnodes/assets/Nodes/BioxelNodes_4.1.blend index f1e08ab..e45e938 100644 --- a/bioxelnodes/assets/Nodes/BioxelNodes_4.1.blend +++ b/bioxelnodes/assets/Nodes/BioxelNodes_4.1.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cc01b98bfee786e9945ba92920e47e58438f5e4a8774af46cc2ac7f7a2d92e4 -size 6230040 +oid sha256:98a47b3c20b3c84bbde76395b182da5a875931dee615192400af495f47927faf +size 6159997 diff --git a/bioxelnodes/customnodes/menus.py b/bioxelnodes/customnodes/menus.py index 9ed668e..5dc4700 100644 --- a/bioxelnodes/customnodes/menus.py +++ b/bioxelnodes/customnodes/menus.py @@ -4,62 +4,12 @@ from .nodes import AddCustomNode -class SaveAllNodes(bpy.types.Operator): - bl_idname = "customnodes.save_all_nodes" - bl_label = "Save All Nodes" - bl_description = "Save All Custom Nodes to Directory." - bl_options = {'UNDO'} - - def execute(self, context): - files = [] - for classname in dir(bpy.types): - if "CUSTOMNODES_MT_NODES_" in classname: - cls = getattr(bpy.types, classname) - files.append(cls.nodes_file) - files = list(set(files)) - - for file in files: - file_name = Path(file).name - # "//" - customnodes_node_dir = bpy.path.abspath(context.scene.customnodes_node_dir) - - output_path: Path = Path(customnodes_node_dir, file_name).resolve() - source_path: Path = Path(file).resolve() - - if output_path != source_path: - shutil.copy(source_path, output_path) - - for lib in bpy.data.libraries: - lib_path = str(Path(bpy.path.abspath(lib.filepath)).resolve()) - if lib_path == file: - blend_path = Path(bpy.path.abspath("//")).resolve() - lib.filepath = bpy.path.relpath( - str(output_path), start=str(blend_path)) - - self.report({"INFO"}, f"Successfully saved to {output_path}") - - return {'FINISHED'} - - -class CUSTOMNODES_PT_CustomNodes(bpy.types.Panel): - bl_idname = "CUSTOMNODES_PT_CustomNodes" - bl_label = "Custom Nodes" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.prop(scene, 'customnodes_node_dir') - layout.operator(SaveAllNodes.bl_idname) - - class CustomNodes(): def __init__( self, menu_items, nodes_file, + class_prefix="CUSTOMNODES_MT_NODES", root_label='CustomNodes', root_icon='NONE' ) -> None: @@ -68,9 +18,10 @@ def __init__( self.menu_items = menu_items self.nodes_file = str(Path(nodes_file).resolve()) - + self.class_prefix = class_prefix self.root_label = root_label self.root_icon = root_icon + self.class_prefix = class_prefix menu_classes = [] self._create_menu_class( @@ -80,7 +31,7 @@ def __init__( ) self.menu_classes = menu_classes - idname = f"CUSTOMNODES_MT_NODES_{root_label.replace(' ', '').upper()}" + idname = f"{class_prefix}_{root_label.replace(' ', '').upper()}" def add_node_menu(self, context): if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type): @@ -92,7 +43,7 @@ def add_node_menu(self, context): def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None): nodes_file = self.nodes_file - idname_namespace = idname_namespace or "CUSTOMNODES_MT_NODES" + idname_namespace = idname_namespace or self.class_prefix idname = f"{idname_namespace}_{label.replace(' ', '').upper()}" # create submenu class if item is menu. @@ -161,7 +112,7 @@ def find_item(self, node_type: str): self._find_item(found_items, self.menu_items, node_type) return found_items[0] if len(found_items) > 0 else None - def add_node(self, node_tree, node_type: str): + def add_node(self, node_group, node_type: str): item = self.find_item(node_type) op = AddCustomNode() op.nodes_file = self.nodes_file @@ -174,7 +125,7 @@ def add_node(self, node_tree, node_type: str): op.node_label = "" op.node_link = True op.node_callback = "" - return op.add_node(node_tree) + return op.add_node(node_group) def register(self): for cls in self.menu_classes: diff --git a/bioxelnodes/customnodes/nodes.py b/bioxelnodes/customnodes/nodes.py index 214cb62..b1d0d44 100644 --- a/bioxelnodes/customnodes/nodes.py +++ b/bioxelnodes/customnodes/nodes.py @@ -85,9 +85,14 @@ def recursive_append_material(self, node_tree): except: ... - def add_node(self, node_tree): + def add_node(self, node_group): + # Deselect all nodes first + for node in node_group.nodes: + if node.select: + node.select = False + self.get_node_tree(self.node_type, self.node_link) - node = node_tree.new("GeometryNodeGroup") + node = node_group.nodes.new("GeometryNodeGroup") self.assign_node_tree(node) return node diff --git a/bioxelnodes/io.py b/bioxelnodes/io.py index 9ead037..7631683 100644 --- a/bioxelnodes/io.py +++ b/bioxelnodes/io.py @@ -13,7 +13,7 @@ from .nodes import custom_nodes from .props import BIOXELNODES_Series from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str, - get_node_by_type, hide_in_ray, lock_transform, save_vdb, show_message) + get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, show_message) try: import SimpleITK as sitk @@ -233,6 +233,8 @@ def execute(self, context): meta['spacing'][2] / orig_spacing[2] * bioxel_size ) + dtype_index = volume.dtype.num + layer_shape = get_layer_shape( bioxel_size, meta['shape'], orig_spacing) @@ -285,10 +287,9 @@ def execute(self, context): ) container = bpy.context.active_object - bbox_verts = calc_bbox_verts((0, 0, 0), volume.shape) + bbox_verts = calc_bbox_verts((0, 0, 0), layer_shape) for index, vert in enumerate(container.data.vertices): - bbox_transform = transfrom - vert.co = bbox_transform @ mathutils.Vector(bbox_verts[index]) + vert.co = transfrom @ mathutils.Vector(bbox_verts[index]) container.matrix_world = mat_ras2blender @ mat_scene_scale container.name = container_name @@ -297,14 +298,14 @@ def execute(self, context): container['bioxel_container'] = True container['scene_scale'] = scene_scale bpy.ops.node.new_geometry_nodes_modifier() - container_node_tree = container.modifiers[0].node_group - input_node = get_node_by_type(container_node_tree.nodes, - 'NodeGroupInput')[0] - container_node_tree.links.remove(input_node.outputs[0].links[0]) + container_node_group = container.modifiers[0].node_group + input_node = get_nodes_by_type(container_node_group, + 'NodeGroupInput')[0] + container_node_group.links.remove(input_node.outputs[0].links[0]) else: container = bpy.data.objects[self.container] - container_node_tree = container.modifiers[0].node_group + container_node_group = container.modifiers[0].node_group preferences = context.preferences.addons[__package__].preferences loc, rot, sca = transfrom.decompose() @@ -316,7 +317,7 @@ def create_layer(volume, layer_name, layer_type="scalar"): grid = vdb.FloatGrid() volume = volume.copy().astype(np.float32) grid.copyFromArray(volume) - grid.transform = vdb.createLinearTransform(transfrom.transposed()) + # grid.transform = vdb.createLinearTransform(transfrom.transposed()) grid.name = layer_type vdb_path = save_vdb([grid], context) @@ -353,26 +354,31 @@ def create_layer(volume, layer_name, layer_type="scalar"): print(f"Creating layer ...") bpy.ops.node.new_geometry_nodes_modifier() - node_tree = layer.modifiers[0].node_group - nodes = node_tree.nodes - links = node_tree.links + node_group = layer.modifiers[0].node_group - input_node = get_node_by_type(nodes, 'NodeGroupInput')[0] - output_node = get_node_by_type(nodes, 'NodeGroupOutput')[0] + input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0] + output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] - to_layer_node = custom_nodes.add_node(nodes, + to_layer_node = custom_nodes.add_node(node_group, "BioxelNodes__ConvertToLayer") - links.new(input_node.outputs[0], to_layer_node.inputs[0]) - links.new(to_layer_node.outputs[0], output_node.inputs[0]) + node_group.links.new(input_node.outputs[0], + to_layer_node.inputs[0]) + node_group.links.new(to_layer_node.outputs[0], + output_node.inputs[0]) + # for compatibility to old vdb + to_layer_node.inputs['Not Transfromed'].default_value = True to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000, 200000000) + to_layer_node.inputs['Data Type'].default_value = dtype_index to_layer_node.inputs['Bioxel Size'].default_value = bioxel_size to_layer_node.inputs['Shape'].default_value = layer_shape to_layer_node.inputs['Origin'].default_value = layer_origin to_layer_node.inputs['Rotation'].default_value = layer_rotation + move_node_between_nodes(to_layer_node, [input_node, output_node]) + return layer if self.read_as == "labels": @@ -386,29 +392,29 @@ def create_layer(volume, layer_name, layer_type="scalar"): for i in range(orig_max): label = volume == np.full_like(volume, i+1) print(f"Resampling...") - label = ski.resize(label.astype(np.float32), + label = ski.resize(label, layer_shape, - anti_aliasing=True) + preserve_range=True, + anti_aliasing=False) layer = create_layer(volume=label, layer_name=f"{container_name}_{layer_name}_{i+1}", layer_type="label") - # Deselect all nodes first - for node in container_node_tree.nodes: - if node.select: - node.select = False - - mask_node = custom_nodes.add_node(container_node_tree.nodes, + mask_node = custom_nodes.add_node(container_node_group, 'BioxelNodes_MaskByLabel') mask_node.label = f"{layer_name}_{i+1}" mask_node.inputs[0].default_value = layer # Connect to output if no output linked - output_node = get_node_by_type(container_node_tree.nodes, - 'NodeGroupOutput')[0] + output_node = get_nodes_by_type(container_node_group, + 'NodeGroupOutput')[0] if len(output_node.inputs[0].links) == 0: - container_node_tree.links.new(mask_node.outputs[0], - output_node.inputs[0]) + container_node_group.links.new(mask_node.outputs[0], + output_node.inputs[0]) + move_node_to_node(mask_node, output_node, (-300, 0)) + else: + move_node_to_node(mask_node, output_node, + (0, -100 * (i+1))) else: if volume.ndim == 4: @@ -420,7 +426,7 @@ def create_layer(volume, layer_name, layer_type="scalar"): # volume = np.multiply(volume, 255.0 / imax_in, dtype=np.float32) # elif volume.dtype.kind == 'i': # volume = volume.astype(np.float32) - + # should not change any value! volume = volume.astype(np.float32) @@ -442,27 +448,27 @@ def create_layer(volume, layer_name, layer_type="scalar"): layer_name=f"{container_name}_{layer_name}", layer_type="scalar") - layer_node_tree = layer.modifiers[0].node_group - to_layer_node = layer_node_tree.nodes['BioxelNodes__ConvertToLayer'] + layer_node_group = layer.modifiers[0].node_group + to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer'] to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset to_layer_node.inputs['Scalar Max'].default_value = orig_max to_layer_node.inputs['Scalar Min'].default_value = orig_min - # Deselect all nodes first - for node in container_node_tree.nodes: - if node.select: - node.select = False - mask_node = custom_nodes.add_node(container_node_tree.nodes, + mask_node = custom_nodes.add_node(container_node_group, 'BioxelNodes_MaskByThreshold') mask_node.label = layer_name mask_node.inputs[0].default_value = layer # Connect to output if no output linked - output_node = get_node_by_type(container_node_tree.nodes, - 'NodeGroupOutput')[0] + output_node = get_nodes_by_type(container_node_group, + 'NodeGroupOutput')[0] + if len(output_node.inputs[0].links) == 0: - container_node_tree.links.new(mask_node.outputs[0], - output_node.inputs[0]) + container_node_group.links.new(mask_node.outputs[0], + output_node.inputs[0]) + move_node_to_node(mask_node, output_node, (-300, 0)) + else: + move_node_to_node(mask_node, output_node, (0, -100)) bpy.context.view_layer.objects.active = container @@ -470,6 +476,7 @@ def create_layer(volume, layer_name, layer_type="scalar"): if preferences.do_change_render_setting and is_first_import: bpy.context.scene.render.engine = 'CYCLES' try: + bpy.context.scene.cycles.shading_system = True bpy.context.scene.cycles.volume_bounces = 12 bpy.context.scene.cycles.transparent_max_bounces = 16 bpy.context.scene.cycles.volume_preview_step_rate = 10 @@ -489,8 +496,8 @@ def invoke(self, context, event): volume_dtype = "Label" elif self.read_as == "scalar": volume_dtype = "Scalar" - title = f"Import '{volume_dtype}' Layer (Add to Container: {self.container})" \ - if self.container != "" else f"Import '{volume_dtype}' Layer (Init a Container)" + title = f"Import as **{volume_dtype}** (Add to Container: {self.container})" \ + if self.container != "" else f"Import as **{volume_dtype}** (Init a Container)" context.window_manager.invoke_props_dialog(self, width=500, title=title) @@ -512,10 +519,9 @@ def draw(self, context): layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m" layout = self.layout - panel = layout.box() if self.container == "": - panel.prop(self, "container_name") - panel.prop(self, "layer_name") + layout.prop(self, "container_name") + layout.prop(self, "layer_name") panel = layout.box() panel.prop(self, "bioxel_size") @@ -575,7 +581,16 @@ def execute(self, context): print("Collecting Meta Data...") volume, meta = parse_volume_data(self.filepath) - do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS + if self.read_as == "labels": + not_int = volume.dtype.kind != "b" and volume.dtype.kind != "i" and volume.dtype.kind != "u" + too_large = np.max(volume) > 100 + + if not_int or too_large: + self.report( + {"WARNING"}, "This volume data does not looks like labels, please check again.") + return {'CANCELLED'} + + # do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS orig_shape = meta['shape'] orig_spacing = meta['spacing'] @@ -601,7 +616,7 @@ def execute(self, context): orig_spacing=orig_spacing, bioxel_size=bioxel_size, series_id=self.series_id or "", - do_orient=do_orient, + # do_orient=do_orient, container=self.container, read_as=self.read_as, scene_scale=scene_scale @@ -619,7 +634,7 @@ def invoke(self, context, event): if not self.filepath and not self.directory: return {'CANCELLED'} - show_message('Reading image data, it may take a while...', + show_message('Parsing volume data, it may take a while...', 'Please be patient...') if get_ext(self.filepath) == '.dcm': @@ -675,7 +690,8 @@ def execute(self, context): bpy.ops.bioxelnodes.parse_volume_data( 'INVOKE_DEFAULT', filepath=self.filepath, - directory=self.directory + directory=self.directory, + read_as=self.read_as ) return {'FINISHED'} @@ -745,7 +761,7 @@ def invoke(self, context, event): class ExportVolumeData(bpy.types.Operator): bl_idname = "bioxelnodes.export_volume_data" - bl_label = "Export Layer" + bl_label = "Export Bioxel as VDB" bl_description = "Export Bioxel Layer as VDB" bl_options = {'UNDO'} diff --git a/bioxelnodes/menus.py b/bioxelnodes/menus.py index 2ee7ea7..e15eedf 100644 --- a/bioxelnodes/menus.py +++ b/bioxelnodes/menus.py @@ -1,8 +1,9 @@ import bpy + from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels, ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange) from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData -from .misc import SaveLayers +from .save import ReLinkNodes, SaveLayers, SaveAllToShare class ModifyLayer(bpy.types.Menu): @@ -20,7 +21,7 @@ def draw(self, context): class AddCutterMenu(bpy.types.Menu): bl_idname = "BIOXELNODES_MT_CUTTERS" - bl_label = "Add Cutter" + bl_label = "Add a Cutter to Container" def draw(self, context): layout = self.layout @@ -32,7 +33,7 @@ def draw(self, context): class ImportLayerMenu(bpy.types.Menu): bl_idname = "BIOXELNODES_MT_LAYERS" - bl_label = "Import Layer" + bl_label = "Import Volume Data as Bioxel" def draw(self, context): layout = self.layout @@ -68,28 +69,28 @@ def draw(self, context): layout.operator(SaveLayers.bl_idname) -# def TOPBAR_FILE_IMPORT(self, context): -# layout = self.layout -# layout.separator() -# layout.operator(ImportVolumeData.bl_idname) +def TOPBAR_FILE_IMPORT(self, context): + layout = self.layout + layout.separator() + layout.menu(ImportLayerMenu.bl_idname) -# def TOPBAR_FILE_EXPORT(self, context): -# layout = self.layout -# layout.separator() -# layout.operator(ExportVolumeData.bl_idname) +def TOPBAR_FILE_EXPORT(self, context): + layout = self.layout + layout.separator() + layout.operator(ExportVolumeData.bl_idname) def VIEW3D_OBJECT(self, context): layout = self.layout - layout.menu(BioxelNodesView3DMenu.bl_idname) layout.separator() + layout.menu(BioxelNodesView3DMenu.bl_idname, icon="FILE_VOLUME") def OUTLINER_OBJECT(self, context): layout = self.layout - layout.menu(BioxelNodesOutlinerMenu.bl_idname) layout.separator() + layout.menu(BioxelNodesOutlinerMenu.bl_idname, icon="FILE_VOLUME") class BioxelNodesTopbarMenu(bpy.types.Menu): @@ -104,7 +105,8 @@ def draw(self, context): layout.menu(AddCutterMenu.bl_idname) layout.operator(ConvertToMesh.bl_idname) layout.separator() - layout.operator(SaveLayers.bl_idname) + layout.operator(SaveAllToShare.bl_idname) + layout.operator(ReLinkNodes.bl_idname) def TOPBAR(self, context): @@ -113,16 +115,16 @@ def TOPBAR(self, context): def add(): - # bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT) - # bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT) - bpy.types.OUTLINER_MT_object.prepend(OUTLINER_OBJECT) - bpy.types.VIEW3D_MT_object_context_menu.prepend(VIEW3D_OBJECT) + bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT) + bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT) + bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT) + bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT) bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR) def remove(): - # bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT) - # bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT) + bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT) + bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT) bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT) bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT) bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR) diff --git a/bioxelnodes/misc.py b/bioxelnodes/misc.py deleted file mode 100644 index 9f42ed0..0000000 --- a/bioxelnodes/misc.py +++ /dev/null @@ -1,92 +0,0 @@ -import bpy -from pathlib import Path -import shutil -from .utils import get_all_layers, get_container, get_container_layers - - -def save_layer(layer, output_dir): - name = layer.name - - # "//" - output_dir = bpy.path.abspath(output_dir) - source_dir = bpy.path.abspath(layer.data.filepath) - - output_path: Path = Path(output_dir, f"{name}.vdb").resolve() - source_path: Path = Path(source_dir).resolve() - - if output_path != source_path: - shutil.copy(source_path, output_path) - - blend_path = Path(bpy.path.abspath("//")).resolve() - layer.data.filepath = bpy.path.relpath( - str(output_path), start=str(blend_path)) - - -class SaveLayers(bpy.types.Operator): - bl_idname = "bioxelnodes.save_layers" - bl_label = "Save Layers" - bl_description = "Save Bioxel Layers to Directory." - bl_options = {'UNDO'} - - @classmethod - def poll(cls, context): - container = get_container(bpy.context.active_object) - return True if container else False - - def execute(self, context): - container = get_container(bpy.context.active_object) - - if not container: - self.report({"WARNING"}, "Cannot find any bioxel container.") - return {'FINISHED'} - - for layer in get_container_layers(container): - try: - save_layer(layer, context.scene.bioxel_layer_dir) - except: - self.report( - {"WARNING"}, f"Fail to save {layer.name}, skiped") - - self.report({"INFO"}, f"Successfully saved bioxel layers.") - - return {'FINISHED'} - - -class SaveAllLayers(bpy.types.Operator): - bl_idname = "bioxelnodes.save_all_layers" - bl_label = "Save All Layers" - bl_description = "Save All Bioxel Layers to Directory." - bl_options = {'UNDO'} - - def execute(self, context): - - layers = get_all_layers() - - if len(layers) == 0: - self.report({"WARNING"}, "Cannot find any bioxel layer.") - return {'FINISHED'} - - for layer in layers: - try: - save_layer(layer, context.scene.bioxel_layer_dir) - except: - self.report( - {"WARNING"}, f"Fail to save {layer.name}, skiped") - - self.report({"INFO"}, f"Successfully saved bioxel layers.") - - return {'FINISHED'} - - -class BIOXELNODES_PT_Bioxels(bpy.types.Panel): - bl_idname = "BIOXELNODES_PT_Bioxels" - bl_label = "Bioxel Nodes" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - - def draw(self, context): - layout = self.layout - scene = context.scene - layout.prop(scene, 'bioxel_layer_dir') - layout.operator(SaveAllLayers.bl_idname) diff --git a/bioxelnodes/nodes.py b/bioxelnodes/nodes.py index 11d3ac7..3ae2530 100644 --- a/bioxelnodes/nodes.py +++ b/bioxelnodes/nodes.py @@ -56,338 +56,206 @@ # return callback_str -if bpy.app.version >= (4, 1, 0): - NODE_FILE = "BioxelNodes_4.1" +NODE_FILE = "BioxelNodes_4.1" - MENU_ITEMS = [ - { - 'label': 'Methods', - 'icon': 'OUTLINER_DATA_VOLUME', - 'items': [ - { - 'label': 'Mask by Threshold', - 'icon': 'EMPTY_SINGLE_ARROW', - 'node_type': 'BioxelNodes_MaskByThreshold', - 'node_description': '' - }, - { - 'label': 'Mask by Range', - 'icon': 'IPO_CONSTANT', - 'node_type': 'BioxelNodes_MaskByRange', - 'node_description': '' - }, - { - 'label': 'Mask by Label', - 'icon': 'MESH_CAPSULE', - 'node_type': 'BioxelNodes_MaskByLabel', - 'node_description': '' - } - ] - }, - { - 'label': 'Shaders', - 'icon': 'SHADING_RENDERED', - 'items': [ - { - 'label': 'Membrane Shader', - 'icon': 'NODE_MATERIAL', - 'node_type': 'BioxelNodes_AssignMembraneShader', - 'node_description': '' - }, - { - 'label': 'Solid Shader', - 'icon': 'SHADING_SOLID', - 'node_type': 'BioxelNodes_AssignSolidShader', - 'node_description': '' - }, - { - 'label': 'Slime Shader', - 'icon': 'OUTLINER_DATA_META', - 'node_type': 'BioxelNodes_AssignSlimeShader', - 'node_description': '' - }, - { - 'label': 'Volume Shader', - 'icon': 'VOLUME_DATA', - 'node_type': 'BioxelNodes_AssignVolumeShader', - 'node_description': '' - }, - { - 'label': 'Universal Shader', - 'icon': 'MATSHADERBALL', - 'node_type': 'BioxelNodes_AssignUniversalShader', - 'node_description': '' - } - ] - }, - { - 'label': 'Colors', - 'icon': 'COLOR', - 'items': [ - { - 'label': 'Color Presets', - 'icon': 'COLOR', - 'node_type': 'BioxelNodes_SetColorPresets', - 'node_description': '' - }, - "separator", - { - 'label': 'Color Ramp 2', - 'icon': 'IPO_QUAD', - 'node_type': 'BioxelNodes_SetColorRamp2', - 'node_description': '' - }, - { - 'label': 'Color Ramp 3', - 'icon': 'IPO_CUBIC', - 'node_type': 'BioxelNodes_SetColorRamp3', - 'node_description': '' - }, - { - 'label': 'Color Ramp 4', - 'icon': 'IPO_QUART', - 'node_type': 'BioxelNodes_SetColorRamp4', - 'node_description': '' - }, - { - 'label': 'Color Ramp 5', - 'icon': 'IPO_QUINT', - 'node_type': 'BioxelNodes_SetColorRamp5', - 'node_description': '' - } - ] - }, - { - 'label': 'Cutters', - 'icon': 'MOD_BEVEL', - 'items': [ - { - 'label': 'Cut', - 'icon': 'MOD_BEVEL', - 'node_type': 'BioxelNodes_Cut', - 'node_description': '' - }, - "separator", - { - 'label': 'Plane Cutter', - 'icon': 'MESH_PLANE', - 'node_type': 'BioxelNodes_PlaneObjectCutter', - 'node_description': '' - }, - { - 'label': 'Cylinder Cutter', - 'icon': 'MESH_CYLINDER', - 'node_type': 'BioxelNodes_CylinderObjectCutter', - 'node_description': '', - }, - { - 'label': 'Cube Cutter', - 'icon': 'MESH_CUBE', - 'node_type': 'BioxelNodes_CubeObjectCutter', - 'node_description': '', - }, - { - 'label': 'Sphere Cutter', - 'icon': 'MESH_UVSPHERE', - 'node_type': 'BioxelNodes_SphereObjectCutter', - 'node_description': '', - } - ] - }, - { - 'label': 'Utils', - 'icon': 'MODIFIER', - 'items': [ - { - 'label': 'Join Component', - 'icon': 'CONSTRAINT_BONE', - 'node_type': 'BioxelNodes_JoinComponent', - 'node_description': '' - }, - "separator", - { - 'label': 'To Mesh', - 'icon': 'OUTLINER_OB_MESH', - 'node_type': 'BioxelNodes_ToMesh', - 'node_description': '' - }, - { - 'label': 'To Volume', - 'icon': 'OUTLINER_OB_VOLUME', - 'node_type': 'BioxelNodes_ToVolume', - 'node_description': '' - }, - { - 'label': 'To Bbox Wire', - 'icon': 'MESH_CUBE', - 'node_type': 'BioxelNodes_ToBboxWire', - 'node_description': '' - }, - "separator", - { - 'label': 'Inflate', - 'icon': 'OUTLINER_OB_META', - 'node_type': 'META_DATA', - 'node_description': '' - }, - { - 'label': 'Smooth', - 'icon': 'MOD_SMOOTH', - 'node_type': 'BioxelNodes_M_Smooth', - 'node_description': '' - }, - { - 'label': 'Remove Small Island', - 'icon': 'FORCE_LENNARDJONES', - 'node_type': 'BioxelNodes_M_RemoveSmallIsland', - 'node_description': '' - } - ] - } - ] - -else: - NODE_FILE = "BioxelNodes_4.0" - MENU_ITEMS = [ - { - 'label': 'Methods', - 'icon': 'OUTLINER_DATA_VOLUME', - 'items': [ - { - 'label': 'Mask by Threshold', - 'icon': 'EMPTY_SINGLE_ARROW', - 'node_type': 'BioxelNodes_MaskByThreshold', - 'node_description': '' - }, - { - 'label': 'Mask by Range', - 'icon': 'IPO_CONSTANT', - 'node_type': 'BioxelNodes_MaskByRange', - 'node_description': '' - }, - { - 'label': 'Mask by Label', - 'icon': 'MESH_CAPSULE', - 'node_type': 'BioxelNodes_MaskByLabel', - 'node_description': '' - } - ] - }, - { - 'label': 'Shaders', - 'icon': 'SHADING_RENDERED', - 'items': [ - { - 'label': 'Solid Shader', - 'icon': 'SHADING_SOLID', - 'node_type': 'BioxelNodes_AssignSolidShader', - 'node_description': '' - }, - { - 'label': 'Slime Shader', - 'icon': 'OUTLINER_OB_META', - 'node_type': 'BioxelNodes_AssignSlimeShader', - 'node_description': '' - }, - { - 'label': 'Volume Shader', - 'icon': 'OUTLINER_OB_VOLUME', - 'node_type': 'BioxelNodes_AssignVolumeShader', - 'node_description': '' - }, - { - 'label': 'Universal Shader', - 'icon': 'SHADING_RENDERED', - 'node_type': 'BioxelNodes_AssignUniversalShader', - 'node_description': '' - } - ] - }, - { - 'label': 'Colors', - 'icon': 'COLOR', - 'items': [ - { - 'label': 'Color Ramp 2', - 'icon': 'IPO_QUAD', - 'node_type': 'BioxelNodes_SetColorRamp2', - 'node_description': '' - }, - { - 'label': 'Color Ramp 3', - 'icon': 'IPO_CUBIC', - 'node_type': 'BioxelNodes_SetColorRamp3', - 'node_description': '' - }, - { - 'label': 'Color Ramp 4', - 'icon': 'IPO_QUART', - 'node_type': 'BioxelNodes_SetColorRamp4', - 'node_description': '' - }, - { - 'label': 'Color Ramp 5', - 'icon': 'IPO_QUINT', - 'node_type': 'BioxelNodes_SetColorRamp5', - 'node_description': '' - } - ] - }, - { - 'label': 'Cutters', - 'icon': 'MOD_BEVEL', - 'items': [ - { - 'label': 'Cut', - 'icon': 'MOD_BEVEL', - 'node_type': 'BioxelNodes_Cut', - 'node_description': '' - }, - "separator", - { - 'label': 'Plane Cutter', - 'icon': 'MESH_PLANE', - 'node_type': 'BioxelNodes_PlaneObjectCutter', - 'node_description': '' - }, - { - 'label': 'Cylinder Cutter', - 'icon': 'MESH_CYLINDER', - 'node_type': 'BioxelNodes_CylinderObjectCutter', - 'node_description': '', - }, - { - 'label': 'Cube Cutter', - 'icon': 'MESH_CUBE', - 'node_type': 'BioxelNodes_CubeObjectCutter', - 'node_description': '', - }, - { - 'label': 'Sphere Cutter', - 'icon': 'MESH_UVSPHERE', - 'node_type': 'BioxelNodes_SphereObjectCutter', - 'node_description': '', - } - ] - }, - { - 'label': 'Utils', - 'icon': 'MODIFIER', - 'items': [ - { - 'label': 'Join Component', - 'icon': 'CONSTRAINT_BONE', - 'node_type': 'BioxelNodes_JoinComponent', - 'node_description': '' - } - ] - } - ] +MENU_ITEMS = [ + { + 'label': 'Methods', + 'icon': 'OUTLINER_DATA_VOLUME', + 'items': [ + { + 'label': 'Mask by Threshold', + 'icon': 'EMPTY_SINGLE_ARROW', + 'node_type': 'BioxelNodes_MaskByThreshold', + 'node_description': '' + }, + { + 'label': 'Mask by Range', + 'icon': 'IPO_CONSTANT', + 'node_type': 'BioxelNodes_MaskByRange', + 'node_description': '' + }, + { + 'label': 'Mask by Label', + 'icon': 'MESH_CAPSULE', + 'node_type': 'BioxelNodes_MaskByLabel', + 'node_description': '' + } + ] + }, + { + 'label': 'Shaders', + 'icon': 'SHADING_RENDERED', + 'items': [ + { + 'label': 'Membrane Shader', + 'icon': 'NODE_MATERIAL', + 'node_type': 'BioxelNodes_AssignMembraneShader', + 'node_description': '' + }, + { + 'label': 'Solid Shader', + 'icon': 'SHADING_SOLID', + 'node_type': 'BioxelNodes_AssignSolidShader', + 'node_description': '' + }, + { + 'label': 'Slime Shader', + 'icon': 'OUTLINER_DATA_META', + 'node_type': 'BioxelNodes_AssignSlimeShader', + 'node_description': '' + }, + { + 'label': 'Volume Shader', + 'icon': 'VOLUME_DATA', + 'node_type': 'BioxelNodes_AssignVolumeShader', + 'node_description': '' + }, + { + 'label': 'Universal Shader', + 'icon': 'MATSHADERBALL', + 'node_type': 'BioxelNodes_AssignUniversalShader', + 'node_description': '' + } + ] + }, + { + 'label': 'Colors', + 'icon': 'COLOR', + 'items': [ + { + 'label': 'Color Presets', + 'icon': 'COLOR', + 'node_type': 'BioxelNodes_ColorPresets', + 'node_description': '' + }, + { + 'label': 'Color Presets MRI', + 'icon': 'COLOR', + 'node_type': 'BioxelNodes_ColorPresets_MRI', + 'node_description': '' + }, + "separator", + { + 'label': 'Color Ramp 2', + 'icon': 'IPO_QUAD', + 'node_type': 'BioxelNodes_SetColorRamp2', + 'node_description': '' + }, + { + 'label': 'Color Ramp 3', + 'icon': 'IPO_CUBIC', + 'node_type': 'BioxelNodes_SetColorRamp3', + 'node_description': '' + }, + { + 'label': 'Color Ramp 4', + 'icon': 'IPO_QUART', + 'node_type': 'BioxelNodes_SetColorRamp4', + 'node_description': '' + }, + { + 'label': 'Color Ramp 5', + 'icon': 'IPO_QUINT', + 'node_type': 'BioxelNodes_SetColorRamp5', + 'node_description': '' + } + ] + }, + { + 'label': 'Cutters', + 'icon': 'MOD_BEVEL', + 'items': [ + { + 'label': 'Cut', + 'icon': 'MOD_BEVEL', + 'node_type': 'BioxelNodes_Cut', + 'node_description': '' + }, + "separator", + { + 'label': 'Plane Cutter', + 'icon': 'MESH_PLANE', + 'node_type': 'BioxelNodes_PlaneObjectCutter', + 'node_description': '' + }, + { + 'label': 'Cylinder Cutter', + 'icon': 'MESH_CYLINDER', + 'node_type': 'BioxelNodes_CylinderObjectCutter', + 'node_description': '', + }, + { + 'label': 'Cube Cutter', + 'icon': 'MESH_CUBE', + 'node_type': 'BioxelNodes_CubeObjectCutter', + 'node_description': '', + }, + { + 'label': 'Sphere Cutter', + 'icon': 'MESH_UVSPHERE', + 'node_type': 'BioxelNodes_SphereObjectCutter', + 'node_description': '', + } + ] + }, + { + 'label': 'Utils', + 'icon': 'MODIFIER', + 'items': [ + { + 'label': 'Join Component', + 'icon': 'CONSTRAINT_BONE', + 'node_type': 'BioxelNodes_JoinComponent', + 'node_description': '' + }, + "separator", + { + 'label': 'Pick Mesh', + 'icon': 'OUTLINER_OB_MESH', + 'node_type': 'BioxelNodes_PickMesh', + 'node_description': '' + }, + { + 'label': 'Pick Volume', + 'icon': 'OUTLINER_OB_VOLUME', + 'node_type': 'BioxelNodes_PickVolume', + 'node_description': '' + }, + { + 'label': 'Pick Bbox Wire', + 'icon': 'MESH_CUBE', + 'node_type': 'BioxelNodes_PickBboxWire', + 'node_description': '' + }, + "separator", + { + 'label': 'Inflate', + 'icon': 'OUTLINER_OB_META', + 'node_type': 'BioxelNodes_M_Inflate', + 'node_description': '' + }, + { + 'label': 'Smooth', + 'icon': 'MOD_SMOOTH', + 'node_type': 'BioxelNodes_M_Smooth', + 'node_description': '' + }, + { + 'label': 'Remove Small Island', + 'icon': 'FORCE_LENNARDJONES', + 'node_type': 'BioxelNodes_M_RemoveSmallIsland', + 'node_description': '' + } + ] + } +] custom_nodes = CustomNodes( menu_items=MENU_ITEMS, nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend", + class_prefix="BIOXELNODES_MT_NODES", root_label='Bioxel Nodes', root_icon="FILE_VOLUME", ) diff --git a/bioxelnodes/operators.py b/bioxelnodes/operators.py index e3967a4..21ab4b6 100644 --- a/bioxelnodes/operators.py +++ b/bioxelnodes/operators.py @@ -8,7 +8,7 @@ from . import scipy from .nodes import custom_nodes from .utils import (get_container, get_container_from_selection, get_container_layers, - get_layer, get_node_by_type, hide_in_ray, lock_transform, save_vdb) + get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb) def get_layer_name(layer): @@ -52,23 +52,22 @@ def set_layer_meta(layer, key: str, value): def add_mask_node(container, layer, node_type: str, node_label: str): modifier = container.modifiers[0] - node_tree = modifier.node_group - - # Deselect all nodes first - for node in node_tree.nodes: - if node.select: - node.select = False - - mask_node = custom_nodes.add_node(node_tree.nodes, node_type) + container_node_group = modifier.node_group + + mask_node = custom_nodes.add_node(container_node_group, node_type) mask_node.label = node_label mask_node.inputs[0].default_value = layer # Connect to output if no output linked - output_node = get_node_by_type(node_tree.nodes, - 'NodeGroupOutput')[0] + output_node = get_nodes_by_type(container_node_group, + 'NodeGroupOutput')[0] + if len(output_node.inputs[0].links) == 0: - node_tree.links.new(mask_node.outputs[0], - output_node.inputs[0]) + container_node_group.links.new(mask_node.outputs[0], + output_node.inputs[0]) + move_node_to_node(mask_node, output_node, (-300, 0)) + else: + move_node_to_node(mask_node, output_node, (0, -100)) return mask_node @@ -106,6 +105,7 @@ def deep_copy_layer(vdb_path, base_layer, name): # add convert to layer node base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer'] + dtype_index = base_layer_node.inputs['Data Type'].default_value bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value layer_shape = base_layer_node.inputs['Shape'].default_value layer_origin = base_layer_node.inputs['Origin'].default_value @@ -115,19 +115,22 @@ def deep_copy_layer(vdb_path, base_layer, name): scalar_max = base_layer_node.inputs['Scalar Max'].default_value bpy.ops.node.new_geometry_nodes_modifier() - node_tree = copyed_layer.modifiers[0].node_group + node_group = copyed_layer.modifiers[0].node_group - input_node = get_node_by_type(node_tree.nodes, 'NodeGroupInput')[0] - output_node = get_node_by_type(node_tree.nodes, 'NodeGroupOutput')[0] + input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0] + output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] - copyed_layer_node = custom_nodes.add_node(node_tree.nodes, + copyed_layer_node = custom_nodes.add_node(node_group, "BioxelNodes__ConvertToLayer") - node_tree.links.new(input_node.outputs[0], copyed_layer_node.inputs[0]) - node_tree.links.new(copyed_layer_node.outputs[0], output_node.inputs[0]) + node_group.links.new(input_node.outputs[0], copyed_layer_node.inputs[0]) + node_group.links.new(copyed_layer_node.outputs[0], output_node.inputs[0]) + # for compatibility to old vdb + copyed_layer_node.inputs['Not Transfromed'].default_value = True copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000, 200000000) + copyed_layer_node.inputs['Data Type'].default_value = dtype_index copyed_layer_node.inputs['Bioxel Size'].default_value = bioxel_size copyed_layer_node.inputs['Shape'].default_value = layer_shape copyed_layer_node.inputs['Origin'].default_value = layer_origin @@ -501,6 +504,7 @@ def execute(self, context): mask = get_volume(label_grids, 0, label_shape) mask = ski.resize(mask, base_shape, + preserve_range=True, anti_aliasing=True) mask = scipy.median_filter(mask.astype(np.float32), size=2) @@ -564,6 +568,7 @@ def execute(self, context): label_volume = get_volume(label_grids, 0, label_shape) label_volume = ski.resize(label_volume, base_shape, + preserve_range=True, anti_aliasing=True) base_volume = np.maximum(base_volume, label_volume) label_names.append(get_layer_name(label)) @@ -586,8 +591,8 @@ def execute(self, context): class ConvertToMesh(bpy.types.Operator): bl_idname = "bioxelnodes.convert_to_mesh" - bl_label = "Convert To Mesh" - bl_description = "Convert Bioxel Components To Mesh" + bl_label = "Convert Container To Mesh" + bl_description = "Convert Bioxel Container To Mesh" bl_options = {'UNDO'} @classmethod @@ -615,21 +620,24 @@ def execute(self, context): bpy.ops.node.new_geometry_nodes_modifier() modifier = mesh.modifiers[0] - node_tree = modifier.node_group + node_group = modifier.node_group - output_node = get_node_by_type(node_tree.nodes, 'NodeGroupOutput')[0] - to_mesh_node = custom_nodes.add_node(node_tree.nodes, - "BioxelNodes_ToMesh") + output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] + to_mesh_node = custom_nodes.add_node(node_group, + "BioxelNodes_PickMesh") to_mesh_node.inputs[0].default_value = container - node_tree.links.new(to_mesh_node.outputs[0], output_node.inputs[0]) + node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0]) - bpy.ops.object.convert(target='MESH') # bpy.ops.constraint.apply( # constraint=mesh.constraints[0].name, owner='OBJECT') + bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name) + bpy.context.object.active_material_index = 1 bpy.ops.object.material_slot_remove() + bpy.context.view_layer.objects.active = mesh + self.report({"INFO"}, f"Successfully convert to mesh") return {'FINISHED'} @@ -681,10 +689,34 @@ def execute(self, context): cutter.display_type = 'WIRE' modifier = container.modifiers[0] - node_tree = modifier.node_group - cutter_node = custom_nodes.add_node(node_tree.nodes, node_type) + node_group = modifier.node_group + cutter_node = custom_nodes.add_node(node_group, node_type) cutter_node.inputs[0].default_value = cutter - + + cut_nodes = get_nodes_by_type(node_group, + 'BioxelNodes_Cut') + output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] + if len(cut_nodes) == 0: + cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut') + if len(output_node.inputs[0].links) == 0: + node_group.links.new(cut_node.outputs[0], + output_node.inputs[0]) + move_node_to_node(cut_node, output_node, (-300, 0)) + else: + pre_output_node = output_node.inputs[0].links[0].from_node + node_group.links.new(pre_output_node.outputs[0], + cut_node.inputs[0]) + node_group.links.new(cut_node.outputs[0], + output_node.inputs[0]) + move_node_between_nodes(cut_node, + [pre_output_node, output_node]) + + node_group.links.new(cutter_node.outputs[0], + cut_node.inputs[1]) + + move_node_to_node(cutter_node, cut_node, (-300, -300)) + else: + move_node_to_node(cutter_node, output_node, (0, -100)) bpy.context.view_layer.objects.active = container return {'FINISHED'} @@ -692,31 +724,31 @@ def execute(self, context): class AddPlaneCutter(bpy.types.Operator, AddCutter): bl_idname = "bioxelnodes.add_plane_cutter" - bl_label = "Add Plane Cutter" - bl_description = "Add Plane Cutter to Container" + bl_label = "Add a Plane Cutter" + bl_description = "Add a Plane Cutter to Container" bl_options = {'UNDO'} object_type = "plane" class AddCylinderCutter(bpy.types.Operator, AddCutter): bl_idname = "bioxelnodes.add_cylinder_cutter" - bl_label = "Add Cylinder Cutter" - bl_description = "Add Cylinder Cutter to Container" + bl_label = "Add a Cylinder Cutter" + bl_description = "Add a Cylinder Cutter to Container" bl_options = {'UNDO'} object_type = "cylinder" class AddCubeCutter(bpy.types.Operator, AddCutter): bl_idname = "bioxelnodes.add_cube_cutter" - bl_label = "Add Cube Cutter" - bl_description = "Add Cube Cutter to Container" + bl_label = "Add a Cube Cutter" + bl_description = "Add a Cube Cutter to Container" bl_options = {'UNDO'} object_type = "cube" class AddSphereCutter(bpy.types.Operator, AddCutter): bl_idname = "bioxelnodes.add_sphere_cutter" - bl_label = "Add Sphere Cutter" - bl_description = "Add Sphere Cutter to Container" + bl_label = "Add a Sphere Cutter" + bl_description = "Add a Sphere Cutter to Container" bl_options = {'UNDO'} object_type = "sphere" diff --git a/bioxelnodes/save.py b/bioxelnodes/save.py new file mode 100644 index 0000000..99cf6a9 --- /dev/null +++ b/bioxelnodes/save.py @@ -0,0 +1,165 @@ +import bpy +from pathlib import Path +import shutil +from .utils import get_all_layers, get_container, get_container_layers +from .nodes import custom_nodes + +CLASS_PREFIX = "BIOXELNODES_MT_NODES" + + +class ReLinkNodes(bpy.types.Operator): + bl_idname = "bioxelnodes.relink_nodes" + bl_label = "Relink Nodes to Addon" + bl_description = "Relink all nodes to addon source" + + def execute(self, context): + file_name = Path(custom_nodes.nodes_file).name + for lib in bpy.data.libraries: + lib_path = Path(bpy.path.abspath(lib.filepath)).resolve() + lib_name = lib_path.name + if lib_name == file_name: + if str(lib_path) != custom_nodes.nodes_file: + lib.filepath = custom_nodes.nodes_file + + self.report({"INFO"}, f"Successfully relinked.") + + return {'FINISHED'} + + +class SaveAllToShare(bpy.types.Operator): + bl_idname = "bioxelnodes.save_all_to_share" + bl_label = "Save All Staged Data" + bl_description = "Save all staged data for sharing" + + layer_dir: bpy.props.StringProperty( + name="Layer Directory", + subtype='DIR_PATH', + default="//" + ) # type: ignore + + save_node: bpy.props.BoolProperty( + name="Save Node Library File", + default=True, + ) # type: ignore + + node_file_dir: bpy.props.StringProperty( + name="Library Directory", + subtype='DIR_PATH', + default="//" + ) # type: ignore + + def execute(self, context): + files = [] + for classname in dir(bpy.types): + if CLASS_PREFIX in classname: + cls = getattr(bpy.types, classname) + files.append(cls.nodes_file) + files = list(set(files)) + + for file in files: + file_name = Path(file).name + # "//" + node_file_dir = bpy.path.abspath(self.node_file_dir) + + output_path: Path = Path(node_file_dir, file_name).resolve() + source_path: Path = Path(file).resolve() + + if output_path != source_path: + shutil.copy(source_path, output_path) + + for lib in bpy.data.libraries: + lib_path = Path(bpy.path.abspath(lib.filepath)).resolve() + if lib_path == source_path: + blend_path = Path(bpy.path.abspath("//")).resolve() + lib.filepath = bpy.path.relpath( + str(output_path), start=str(blend_path)) + + self.report({"INFO"}, f"Successfully saved to {output_path}") + + layers = get_all_layers() + for layer in layers: + try: + save_layer(layer, self.layer_dir) + except: + self.report( + {"WARNING"}, f"Fail to save {layer.name}, skiped") + + self.report({"INFO"}, f"Successfully saved bioxel layers.") + + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.invoke_props_dialog(self, + width=500) + return {'RUNNING_MODAL'} + + @classmethod + def poll(cls, context): + return bpy.data.is_saved + + def draw(self, context): + layout = self.layout + panel = layout.box() + panel.prop(self, "layer_dir") + panel = layout.box() + panel.prop(self, "save_node") + panel.prop(self, "node_file_dir") + layout.label(text="Save your blender file first.") + + +def save_layer(layer, output_dir): + name = layer.name + + # "//" + output_dir = bpy.path.abspath(output_dir) + source_dir = bpy.path.abspath(layer.data.filepath) + + output_path: Path = Path(output_dir, f"{name}.vdb").resolve() + source_path: Path = Path(source_dir).resolve() + + if output_path != source_path: + shutil.copy(source_path, output_path) + + blend_path = Path(bpy.path.abspath("//")).resolve() + layer.data.filepath = bpy.path.relpath( + str(output_path), start=str(blend_path)) + + +class SaveLayers(bpy.types.Operator): + bl_idname = "bioxelnodes.save_layers" + bl_label = "Save Container's Layers" + bl_description = "Save Bioxel Layers to Directory." + + layer_dir: bpy.props.StringProperty( + name="Layer Directory", + subtype='DIR_PATH', + default="//" + ) # type: ignore + + @classmethod + def poll(cls, context): + container = get_container(bpy.context.active_object) + return True if container else False + + def execute(self, context): + container = get_container(bpy.context.active_object) + + if not container: + self.report({"WARNING"}, "Cannot find any bioxel container.") + return {'FINISHED'} + + for layer in get_container_layers(container): + try: + save_layer(layer, self.layer_dir) + except: + self.report( + {"WARNING"}, f"Fail to save {layer.name}, skiped") + + self.report({"INFO"}, f"Successfully saved bioxel layers.") + + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.invoke_props_dialog(self, + width=500) + return {'RUNNING_MODAL'} diff --git a/bioxelnodes/utils.py b/bioxelnodes/utils.py index c8928ef..c3a8b1e 100644 --- a/bioxelnodes/utils.py +++ b/bioxelnodes/utils.py @@ -5,12 +5,32 @@ from uuid import uuid4 -def get_type(cls): - return type(cls).__name__ +def move_node_to_node(node, target_node, offset=(0, 0)): + node.location.x = target_node.location.x + offset[0] + node.location.y = target_node.location.y + offset[1] -def get_node_by_type(nodes, type_name: str): - return [node for node in nodes if get_type(node) == type_name] +def move_node_between_nodes(node, target_nodes, offset=(0, 0)): + xs = [] + ys = [] + for target_node in target_nodes: + xs.append(target_node.location.x) + ys.append(target_node.location.y) + + node.location.x = sum(xs) / len(xs) + offset[0] + node.location.y = sum(ys) / len(ys) + offset[1] + + +def get_node_type(node): + node_type = type(node).__name__ + if node_type == "GeometryNodeGroup": + node_type = node.node_tree.name + + return node_type + + +def get_nodes_by_type(node_group, type_name: str): + return [node for node in node_group.nodes if get_node_type(node) == type_name] def show_message(message="", title="Message Box", icon='INFO'): diff --git a/extension/__init__.py b/extension/__init__.py index 986cf2c..6be166f 100644 --- a/extension/__init__.py +++ b/extension/__init__.py @@ -10,11 +10,6 @@ def register(): auto_load.register() menus.add() - bpy.types.Scene.bioxel_layer_dir = bpy.props.StringProperty( - name="Layer Directory", - subtype='DIR_PATH', - default="//" - ) def unregister(): diff --git a/extension/blender_manifest.toml b/extension/blender_manifest.toml index aba1f8f..1dfcbb3 100644 --- a/extension/blender_manifest.toml +++ b/extension/blender_manifest.toml @@ -3,7 +3,7 @@ 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.4" +version = "0.2.5" name = "Bioxel Nodes" tagline = "For scientific volumetric data visualization in Blender" maintainer = "Ma Nan " diff --git a/pyproject.toml b/pyproject.toml index 7b7e917..dc265e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bioxelnodes" -version = "0.2.4" +version = "0.2.5" description = "" authors = ["Ma Nan "] license = "MIT"