feat: dramatic reduction in rendering time 🚀
feat: add more support to eevee render fix: fail to add utils node inflate fix: file import button missing chore: change operator names BREAKING CHANGE: remove support to blender 4.0 BREAKING CHANGE: remove support to gpu render with cuda
This commit is contained in:
@@ -8,8 +8,8 @@ bl_info = {
|
||||
"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 @@ auto_load.init()
|
||||
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():
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -4,62 +4,12 @@ from pathlib import Path
|
||||
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 @@ class CustomNodes():
|
||||
|
||||
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 @@ class CustomNodes():
|
||||
)
|
||||
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 @@ class CustomNodes():
|
||||
|
||||
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 @@ class CustomNodes():
|
||||
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 @@ class CustomNodes():
|
||||
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:
|
||||
|
||||
@@ -85,9 +85,14 @@ class AddCustomNode():
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ from . import skimage as ski
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
)
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
# 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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
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 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
|
||||
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 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
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 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
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 @@ class ImportVolumeData():
|
||||
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 @@ class AddVolumeData(bpy.types.Operator):
|
||||
|
||||
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'}
|
||||
|
||||
|
||||
@@ -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 @@ class ModifyLayer(bpy.types.Menu):
|
||||
|
||||
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 @@ class AddCutterMenu(bpy.types.Menu):
|
||||
|
||||
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 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
||||
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 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
@@ -56,338 +56,206 @@ import bpy
|
||||
# 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",
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from . import skimage as ski
|
||||
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 @@ class FillByLabel(bpy.types.Operator):
|
||||
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 @@ class CombineLabels(bpy.types.Operator):
|
||||
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 @@ class CombineLabels(bpy.types.Operator):
|
||||
|
||||
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 @@ class ConvertToMesh(bpy.types.Operator):
|
||||
|
||||
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 @@ class AddCutter():
|
||||
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 @@ class AddCutter():
|
||||
|
||||
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"
|
||||
|
||||
165
bioxelnodes/save.py
Normal file
165
bioxelnodes/save.py
Normal file
@@ -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'}
|
||||
@@ -5,12 +5,32 @@ import pyopenvdb as vdb
|
||||
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'):
|
||||
|
||||
Reference in New Issue
Block a user