feat: layer list system

This commit is contained in:
2024-08-14 14:16:40 +08:00
parent 21e0b85662
commit 9e885865f7
21 changed files with 1451 additions and 825 deletions

2
.gitignore vendored
View File

@@ -133,4 +133,6 @@ blendcache_*
.secrets
.vdb
!scipy_ndimage/*/**

View File

@@ -1,5 +1,6 @@
import bpy
from .props import BIOXELNODES_LayerListUL
from . import auto_load
from . import menus
@@ -9,11 +10,17 @@ auto_load.init()
def register():
auto_load.register()
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty()
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty(
default=1.0)
bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty()
bpy.types.WindowManager.bioxelnodes_layer_list_UL = bpy.props.PointerProperty(
type=BIOXELNODES_LayerListUL)
menus.add()
def unregister():
menus.remove()
del bpy.types.WindowManager.bioxelnodes_progress_factor
del bpy.types.WindowManager.bioxelnodes_progress_text
del bpy.types.WindowManager.bioxelnodes_layer_list_UL
auto_load.unregister()

View File

@@ -7,7 +7,6 @@ from . import scipy as ndi
# 3rd-party
import transforms3d
# TODO: turn to dataclasses
@@ -105,13 +104,13 @@ class Layer():
data = self.data
order = 0 if self.dtype == bool else 1
# TXYZC > TXYZ
if self.kind in ['label', 'scalar']:
data = np.amax(data, -1)
# # TXYZC > TXYZ
# if self.kind in ['label', 'scalar']:
# data = np.amax(data, -1)
# if self.kind in ['scalar']:
# dtype = data.dtype
# data = data.astype(np.float32)
# data = data.astype(np.float32)
data_frames = ()
for f in range(self.frame_count):
@@ -125,8 +124,10 @@ class Layer():
factors = np.divide(self.shape, shape)
zoom_factors = [1 / f for f in factors]
frame = ndi.zoom(data[f, :, :, :],
zoom_factors,
frame = ndi.zoom(data[f, :, :, :, :],
zoom_factors+[1.0],
mode="nearest",
grid_mode=False,
order=order)
data_frames += (frame,)
@@ -137,7 +138,10 @@ class Layer():
# data = data.astype(dtype)
# TXYZ > TXYZC
if self.kind in ['label', 'scalar']:
data = np.expand_dims(data, axis=-1) # expend channel
# if self.kind in ['label', 'scalar']:
# data = np.expand_dims(data, axis=-1) # expend channel
self.data = data
mat_scale = transforms3d.zooms.zfdir2aff(factors[0])
self.affine = np.dot(self.affine, mat_scale)

View File

@@ -42,7 +42,8 @@ DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA']
SEQUENCE_EXTS = ['.bmp', '.BMP',
'.jpg', '.JPG', '.jpeg', '.JPEG',
'.tif', '.TIF', '.tiff', '.TIFF',
'.png', '.PNG']
'.png', '.PNG',
'.mrc']
def get_ext(filepath: Path) -> str:
@@ -64,18 +65,40 @@ def get_ext(filepath: Path) -> str:
return filepath.suffix
def get_file_name(filepath: Path):
def get_filename(filepath: Path):
ext = get_ext(filepath)
return filepath.name.removesuffix(ext).replace(" ", "-")
return filepath.name.removesuffix(ext)
def get_file_number(filepath: Path) -> str:
name = get_file_name(filepath)
def get_filename_parts(filepath: Path) -> str:
def has_digits(s):
return any(char.isdigit() for char in s)
name = get_filename(filepath)
parts = name.replace(".", " ").replace("_", " ").split(" ")
skip_prefixs = ["CH", "ch", "channel"]
number_part = None
number_part_i = None
for i, part in enumerate(parts[::-1]):
if has_digits(part):
if not any([part.startswith(prefix) for prefix in skip_prefixs]):
number_part = part
number_part_i = len(parts)-i
break
if number_part is None:
return name, "", ""
prefix_parts = parts[:number_part_i-1]
prefix_parts_count = sum([len(part)+1 for part in prefix_parts])
digits = ""
suffix = ""
# Iterate through the characters in reverse order
started = False
for char in name[::-1]:
for char in number_part[::-1]:
if char.isdigit():
started = True
# If the character is a digit, add it to the digits string
@@ -84,20 +107,33 @@ def get_file_number(filepath: Path) -> str:
if started:
# If a non-digit character is encountered, stop the loop
break
else:
suffix += char
digits = digits[::-1]
prefix_parts_count += len(number_part) - \
len(digits) - len(suffix)
# Reverse the digits string to get the correct order
return digits[::-1]
prefix = name[:prefix_parts_count]
suffix = name[prefix_parts_count+len(digits):]
return prefix, digits, suffix
def get_sequence_name(filepath: Path) -> str:
name = get_file_name(filepath)
number = get_file_number(filepath)
return name.removesuffix(number)
def get_file_no_digits_name(filepath: Path) -> str:
prefix, digits, suffix = get_filename_parts(filepath)
prefix = remove_end_str(prefix, "_")
prefix = remove_end_str(prefix, ".")
prefix = remove_end_str(prefix, "-")
prefix = remove_end_str(prefix, " ")
return prefix + suffix
def get_sequence_index(filepath: Path) -> int:
number = get_file_number(filepath)
return int(number) if number != "" else 0
def get_file_index(filepath: Path) -> int:
prefix, digits, suffix = get_filename_parts(filepath)
return int(digits) if digits != "" else 0
def collect_sequence(filepath: Path):
@@ -105,8 +141,8 @@ def collect_sequence(filepath: Path):
for f in filepath.parent.iterdir():
if f.is_file() \
and get_ext(filepath) == get_ext(f) \
and get_sequence_name(filepath) == get_sequence_name(f):
index = get_sequence_index(f)
and get_file_no_digits_name(filepath) == get_file_no_digits_name(f):
index = get_file_index(f)
file_dict[index] = f
# reomve isolated seq file
@@ -124,6 +160,12 @@ def collect_sequence(filepath: Path):
return sequence
def remove_end_str(string: str, end: str):
while string.endswith(end) and len(string) > 0:
string = string.removesuffix(end)
return string
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None) -> Layer:
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
@@ -139,7 +181,7 @@ def parse_volumetric_data(data_file: str, series_id="", progress_callback=None)
ext = get_ext(data_path)
if progress_callback:
progress_callback(0, "Reading the Data...")
progress_callback(0.0, "Reading the Data...")
is_sequence = False
if ext in SEQUENCE_EXTS:
@@ -181,7 +223,7 @@ def parse_volumetric_data(data_file: str, series_id="", progress_callback=None)
elif mrc.is_volume_stack():
data = np.expand_dims(data, axis=-1) # expend channel
name = get_file_name(data_path)
name = get_file_no_digits_name(data_path)
spacing = (mrc.voxel_size.x,
mrc.voxel_size.y,
mrc.voxel_size.z)
@@ -245,7 +287,7 @@ def parse_volumetric_data(data_file: str, series_id="", progress_callback=None)
except:
...
name = get_file_name(data_path)
name = get_file_no_digits_name(data_path)
except:
...
@@ -297,10 +339,10 @@ def parse_volumetric_data(data_file: str, series_id="", progress_callback=None)
elif ext in SEQUENCE_EXTS and is_sequence:
itk_image = sitk.ReadImage(sequence)
name = get_sequence_name(data_path)
name = get_file_no_digits_name(data_path)
else:
itk_image = sitk.ReadImage(data_path)
name = get_file_name(data_path)
name = get_filename(data_path)
# for key in itk_image.GetMetaDataKeys():
# print(f"{key},{itk_image.GetMetaData(key)}")

View File

@@ -5,9 +5,12 @@ from bpy_extras.io_utils import axis_conversion
from mathutils import Matrix, Vector
from .layer import Layer, get_container_layer_objs, layer_to_obj, obj_to_layer
from .layer import Layer, layer_to_obj, obj_to_layer
from ..nodes import custom_nodes
from .node import get_nodes_by_type, move_node_to_node
from .utils import (get_container_layer_objs,
get_layer_prop_value,
get_nodes_by_type,
move_node_to_node)
NODE_TYPE = {
@@ -66,51 +69,6 @@ def calc_bbox_verts(origin: tuple, size: tuple):
return bbox_verts
def get_container_objs_from_selection():
container_objs = []
for obj in bpy.context.selected_objects:
if get_container_obj(obj):
container_objs.append(obj)
return list(set(container_objs))
def get_container_obj(current_obj):
if current_obj:
if current_obj.get('bioxel_container'):
return current_obj
elif current_obj.get('bioxel_layer'):
parent = current_obj.parent
return parent if parent.get('bioxel_container') else None
return None
def add_layers(layers: list[Layer],
container_obj: bpy.types.Object,
cache_dir: str):
container_node_group = container_obj.modifiers[0].node_group
for i, layer in enumerate(layers):
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
mask_node = custom_nodes.add_node(container_node_group,
NODE_TYPE[layer.kind])
mask_node.label = layer_obj.name
mask_node.inputs[0].default_value = layer_obj
# Connect to output if no output linked
output_node = get_nodes_by_type(container_node_group,
'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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)))
return container_obj
def obj_to_container(container_obj: bpy.types.Object):
layer_objs = get_container_layer_objs(container_obj)
layers = [obj_to_layer(obj) for obj in layer_objs]
@@ -119,6 +77,31 @@ def obj_to_container(container_obj: bpy.types.Object):
return container
def add_layers(layers: list[Layer],
container_obj: bpy.types.Object,
cache_dir: str):
node_group = container_obj.modifiers[0].node_group
output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0]
for i, layer in enumerate(layers):
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
fetch_node = custom_nodes.add_node(node_group,
"BioxelNodes_FetchLayer")
fetch_node.label = get_layer_prop_value(layer_obj, "name")
fetch_node.inputs[0].default_value = layer_obj
if len(output_node.inputs[0].links) == 0:
node_group.links.new(fetch_node.outputs[0],
output_node.inputs[0])
move_node_to_node(fetch_node, output_node, (-600, 0))
else:
move_node_to_node(fetch_node, output_node, (0, -100 * (i+1)))
return container_obj
def container_to_obj(container: Container,
scene_scale: float,
cache_dir: str):
@@ -153,28 +136,16 @@ def container_to_obj(container: Container,
container_obj.show_in_front = True
container_obj['bioxel_container'] = True
bpy.ops.node.new_geometry_nodes_modifier()
container_node_group = container_obj.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])
modifier = container_obj.modifiers.new("GeometryNodes", 'NODES')
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
node_group.interface.new_socket(name="Component",
in_out="OUTPUT",
socket_type="NodeSocketGeometry")
modifier.node_group = node_group
node_group.nodes.new("NodeGroupOutput")
for i, layer in enumerate(container.layers):
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
mask_node = custom_nodes.add_node(container_node_group,
NODE_TYPE[layer.kind])
mask_node.label = layer_obj.name
mask_node.inputs[0].default_value = layer_obj
# Connect to output if no output linked
output_node = get_nodes_by_type(container_node_group,
'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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)))
container_obj = add_layers(container.layers,
container_obj=container_obj,
cache_dir=cache_dir)
return container_obj

View File

@@ -9,33 +9,7 @@ from uuid import uuid4
from ..nodes import custom_nodes
from ..bioxel.layer import Layer
from .node import get_nodes_by_type, move_node_between_nodes
def get_layer_obj(current_obj: bpy.types.Object):
if current_obj:
if current_obj.get('bioxel_layer') and current_obj.parent:
if current_obj.parent.get('bioxel_container'):
return current_obj
return None
def get_container_layer_objs(container_obj: bpy.types.Object):
layer_objs = []
for obj in bpy.context.scene.objects:
if obj.parent == container_obj and get_layer_obj(obj):
layer_objs.append(obj)
return layer_objs
def get_all_layer_objs():
layer_objs = []
for obj in bpy.context.scene.objects:
if get_layer_obj(obj):
layer_objs.append(obj)
return layer_objs
from .utils import get_layer_prop_value, move_node_between_nodes
def obj_to_layer(layer_obj: bpy.types.Object):
@@ -63,11 +37,16 @@ def obj_to_layer(layer_obj: bpy.types.Object):
grid.copyToArray(data)
data = np.expand_dims(data, axis=0) # expend frame
name = metadata["layer_name"]
kind = metadata["layer_kind"]
name = get_layer_prop_value(layer_obj, "name") \
or metadata["layer_name"]
kind = get_layer_prop_value(layer_obj, "kind") \
or metadata["layer_kind"]
affine = metadata["layer_affine"]
dtype = metadata.get("data_dtype") or "float32"
offset = metadata.get("data_offset") or 0
dtype = get_layer_prop_value(layer_obj, "dtype") \
or metadata.get("data_dtype") or "float32"
offset = get_layer_prop_value(layer_obj, "offset") \
or metadata.get("data_offset") or 0
data = data - np.full_like(data, offset)
data = data.astype(dtype)
@@ -110,6 +89,7 @@ def layer_to_obj(layer: Layer,
"data_offset": offset
}
layer_display_name = f"{container_obj.name}_{layer.name}"
if layer.frame_count > 1:
print(f"Saving the Cache of {layer.name}...")
vdb_name = str(uuid4())
@@ -118,8 +98,14 @@ def layer_to_obj(layer: Layer,
cache_filepaths = []
for f in range(layer.frame_count):
grid = vdb.FloatGrid()
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
if layer.kind in ['label', 'scalar']:
grid = vdb.FloatGrid()
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
else:
# color
grid = vdb.Vec3SGrid()
grid.copyFromArray(
data[f, :, :, :, :].copy().astype(np.float32))
grid.transform = vdb.createLinearTransform(
layer.affine.transpose())
grid.metadata = metadata
@@ -130,17 +116,14 @@ def layer_to_obj(layer: Layer,
vdb.write(str(cache_filepath), grids=[grid])
cache_filepaths.append(cache_filepath)
print(f"Loading the Cache of {layer.name}...")
files = [{"name": str(cache_filepath.name), "name": str(cache_filepath.name)}
for cache_filepath in cache_filepaths]
bpy.ops.object.volume_import(filepath=str(cache_filepaths[0]),
directory=str(cache_filepaths[0].parent),
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
else:
grid = vdb.FloatGrid()
grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32))
if layer.kind in ['label', 'scalar']:
grid = vdb.FloatGrid()
grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32))
else:
# color
grid = vdb.Vec3SGrid()
grid.copyFromArray(data[0, :, :, :, :].copy().astype(np.float32))
grid.transform = vdb.createLinearTransform(
layer.affine.transpose())
grid.metadata = metadata
@@ -151,52 +134,33 @@ def layer_to_obj(layer: Layer,
vdb.write(str(cache_filepath), grids=[grid])
cache_filepaths = [cache_filepath]
print(f"Loading the Cache of {layer.name}...")
bpy.ops.object.volume_import(filepath=str(cache_filepaths[0]),
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
layer_data = bpy.data.volumes.new(layer_display_name)
layer_data.sequence_mode = 'REPEAT'
layer_data.filepath = str(cache_filepaths[0])
layer_obj = bpy.context.active_object
layer_obj.data.sequence_mode = 'REPEAT'
if layer.frame_count > 1:
layer_data.is_sequence = True
layer_data.frame_duration = layer.frame_count
else:
layer_data.is_sequence = False
# Set props to VDB object
layer_obj.name = f"{container_obj.name}_{layer.name}"
layer_obj.data.name = f"{container_obj.name}_{layer.name}"
layer_obj.lock_location[0] = True
layer_obj.lock_location[1] = True
layer_obj.lock_location[2] = True
layer_obj.lock_rotation[0] = True
layer_obj.lock_rotation[1] = True
layer_obj.lock_rotation[2] = True
layer_obj.lock_scale[0] = True
layer_obj.lock_scale[1] = True
layer_obj.lock_scale[2] = True
layer_obj.visible_camera = False
layer_obj.visible_diffuse = False
layer_obj.visible_glossy = False
layer_obj.visible_transmission = False
layer_obj.visible_volume_scatter = False
layer_obj.visible_shadow = False
layer_obj.hide_select = True
layer_obj.hide_render = True
layer_obj.hide_viewport = True
layer_obj = bpy.data.objects.new(layer_display_name, layer_data)
layer_obj['bioxel_layer'] = True
layer_obj.parent = container_obj
for collection in layer_obj.users_collection:
collection.objects.unlink(layer_obj)
for collection in container_obj.users_collection:
collection.objects.link(layer_obj)
print(f"Creating Node for {layer.name}...")
bpy.ops.node.new_geometry_nodes_modifier()
node_group = layer_obj.modifiers[0].node_group
modifier = layer_obj.modifiers.new("GeometryNodes", 'NODES')
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
node_group.interface.new_socket(name="Cache",
in_out="INPUT",
socket_type="NodeSocketGeometry")
node_group.interface.new_socket(name="Layer",
in_out="OUTPUT",
socket_type="NodeSocketGeometry")
modifier.node_group = node_group
layer_node = custom_nodes.add_node(node_group,
"BioxelNodes__Layer")
"BioxelNodes__Layer")
layer_node.inputs['name'].default_value = layer.name
layer_node.inputs['shape'].default_value = layer.shape
@@ -208,7 +172,7 @@ def layer_to_obj(layer: Layer,
layer_node.inputs[affine_key].default_value = layer.affine[j, i]
layer_node.inputs['id'].default_value = random.randint(-200000000,
200000000)
200000000)
layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0]
layer_node.inputs['dtype'].default_value = layer.dtype.str
layer_node.inputs['dtype_num'].default_value = layer.dtype.num
@@ -216,10 +180,8 @@ def layer_to_obj(layer: Layer,
layer_node.inputs['min'].default_value = layer.min
layer_node.inputs['max'].default_value = layer.max
input_node = get_nodes_by_type(node_group,
'NodeGroupInput')[0]
output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0]
input_node = node_group.nodes.new("NodeGroupInput")
output_node = node_group.nodes.new("NodeGroupOutput")
node_group.links.new(input_node.outputs[0],
layer_node.inputs[0])
@@ -229,4 +191,6 @@ def layer_to_obj(layer: Layer,
move_node_between_nodes(
layer_node, [input_node, output_node])
layer_obj.parent = container_obj
return layer_obj

View File

@@ -1,26 +0,0 @@
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 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]

View File

@@ -0,0 +1,96 @@
import bpy
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 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 get_container_objs_from_selection():
container_objs = []
for obj in bpy.context.selected_objects:
if get_container_obj(obj):
container_objs.append(obj)
return list(set(container_objs))
def get_container_obj(current_obj):
if current_obj:
if current_obj.get('bioxel_container'):
return current_obj
elif current_obj.get('bioxel_layer'):
parent = current_obj.parent
return parent if parent.get('bioxel_container') else None
return None
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
node_group = layer_obj.modifiers[0].node_group
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
value = layer_node.inputs[prop].default_value
if type(value).__name__ == "bpy_prop_array":
value = tuple(value)
return tuple([int(v) for v in value]) \
if prop in ["shape"] else value
elif type(value).__name__ == "str":
return str(value)
if type(value).__name__ == "float":
value = float(value)
return round(value, 2) \
if prop in ["bioxel_size"] else value
def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value):
node_group = layer_obj.modifiers[0].node_group
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
layer_node.inputs[prop].default_value = value
def get_layer_obj(current_obj: bpy.types.Object):
if current_obj:
if current_obj.get('bioxel_layer') and current_obj.parent:
if current_obj.parent.get('bioxel_container'):
return current_obj
return None
def get_container_layer_objs(container_obj: bpy.types.Object):
layer_objs = []
for obj in bpy.data.objects:
if obj.parent == container_obj and get_layer_obj(obj):
layer_objs.append(obj)
return layer_objs
def get_all_layer_objs():
layer_objs = []
for obj in bpy.data.objects:
if get_layer_obj(obj):
layer_objs.append(obj)
return layer_objs

View File

@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "bioxelnodes"
version = "0.3.3"
version = "0.4.0"
name = "Bioxel Nodes"
tagline = "For scientific volumetric data visualization in Blender"
maintainer = "Ma Nan <icrdr2010@outlook.com>"
@@ -30,9 +30,8 @@ wheels = [
"./wheels/tifffile-2024.7.24-py3-none-any.whl",
"./wheels/pyometiff-1.0.0-py3-none-any.whl",
"./wheels/mrcfile-1.5.1-py2.py3-none-any.whl",
"./wheels/transforms3d-0.4.2-py3-none-any.whl",
"./wheels/transforms3d-0.4.2-py3-none-any.whl"
]
[permissions]
files = "Import/export volume data from/to disk"

View File

@@ -25,7 +25,7 @@ class AddCustomNode():
node_description: bpy.props.StringProperty(
name="node_description",
description="",
default="Add custom node group.",
default="",
subtype="NONE"
) # type: ignore
@@ -70,20 +70,10 @@ class AddCustomNode():
node_tree = bpy.data.node_groups.get(node_type)
if node_tree:
# self.recursive_append_material(node_tree)
return node_tree
else:
raise RuntimeError('No custom node found')
def recursive_append_material(self, node_tree):
for child in node_tree.nodes:
material_socket = child.inputs.get('Material')
if material_socket:
print(material_socket.default_value)
try:
self.recursive_append_material(child.node_tree)
except:
...
def add_node(self, node_group):
# Deselect all nodes first

View File

@@ -1,21 +1,22 @@
from pathlib import Path
import bpy
from .bioxelutils.container import get_container_objs_from_selection
from .operators.layer import (CombineLabels, SignScalar,
FillByLabel, FillByThreshold, FillByRange)
from .operators.container import (SaveContainer, LoadContainer,
from .operators.utils import get_layer_item_label
from .bioxelutils.utils import (get_container_obj,
get_container_objs_from_selection,
get_container_layer_objs,
get_layer_prop_value)
from .operators.layer import (FetchLayerMenu, FetchLayer,
RemoveLayer, RemoveMissingLayers, RenameLayer, ResampleScalar, SaveLayerCache,
SignScalar, CombineLabels,
FillByLabel, FillByThreshold, FillByRange, get_selected_objs_in_node_tree)
from .operators.container import (SaveAllLayerCaches, SaveContainer, LoadContainer,
AddPieCutter, AddPlaneCutter,
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
PickBboxWire, PickMesh, PickVolume)
from .operators.io import (ExportVolumetricData,
ImportAsLabelLayer, ImportAsScalarLayer)
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
from .operators.misc import (CleanAllCaches,
ReLinkNodes, SaveCaches, SaveStagedData)
def container_is_selected():
container_objs = get_container_objs_from_selection()
return len(container_objs) > 0
ReLinkNodes, RenderSettingPreset, SaveStagedData)
class PickFromContainerMenu(bpy.types.Menu):
@@ -29,22 +30,9 @@ class PickFromContainerMenu(bpy.types.Menu):
layout.operator(PickBboxWire.bl_idname)
class ModifyLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_MODIFY_LAYERS"
bl_label = "Modify Layer"
def draw(self, context):
layout = self.layout
layout.operator(SignScalar.bl_idname)
layout.operator(FillByThreshold.bl_idname)
layout.operator(FillByRange.bl_idname)
layout.operator(FillByLabel.bl_idname)
layout.operator(CombineLabels.bl_idname)
class AddCutterMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_CUTTERS"
bl_label = "Add a Cutter to Container"
bl_label = "Add a Object Cutter"
def draw(self, context):
layout = self.layout
@@ -56,101 +44,78 @@ class AddCutterMenu(bpy.types.Menu):
class ImportLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_LAYERS"
bl_label = "Import Volumetric Data"
bl_idname = "BIOXELNODES_MT_IMPORTLAYER"
bl_label = "Import Volumetric Data (Init)"
bl_icon = "FILE_NEW"
def draw(self, context):
layout = self.layout
layout.operator(ImportAsScalarLayer.bl_idname, text="as Scalar")
layout.operator(ImportAsLabelLayer.bl_idname, text="as Label")
layout.operator(ImportAsScalar.bl_idname,
text="as Scalar")
layout.operator(ImportAsLabel.bl_idname,
text="as Label")
layout.operator(ImportAsColor.bl_idname,
text="as Color")
class BioxelNodesView3DMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_VIEW3D"
bl_label = "Bioxel Nodes"
class AddLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_ADDLAYER"
bl_label = "Import Volumetric Data (Add to)"
bl_icon = "FILE_NEW"
def draw(self, context):
layout = self.layout
is_selected = container_is_selected()
layout.operator(ImportAsScalarLayer.bl_idname,
text=ImportAsScalarLayer.bl_label+" (Add to)"
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
layout.operator(ImportAsLabelLayer.bl_idname,
text=ImportAsLabelLayer.bl_label+" (Add to)"
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
layout.separator()
layout.operator(AddPlaneCutter.bl_idname)
layout.operator(AddCylinderCutter.bl_idname)
layout.operator(AddCubeCutter.bl_idname)
layout.operator(AddSphereCutter.bl_idname)
layout.operator(AddPieCutter.bl_idname)
layout.separator()
layout.operator(PickMesh.bl_idname)
layout.operator(PickVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname)
layout.separator()
layout.operator(SaveCaches.bl_idname)
layout.operator(ImportAsScalar.bl_idname,
text="as Scalar")
layout.operator(ImportAsLabel.bl_idname,
text="as Label")
layout.operator(ImportAsColor.bl_idname,
text="as Color")
class BioxelNodesOutlinerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_OUTLINER"
bl_label = "Bioxel Nodes"
class ModifyLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_MODIFYLAYER"
bl_label = "Modify Layer"
bl_icon = "FILE_NEW"
def draw(self, context):
layer_objs = get_selected_objs_in_node_tree(context)
if len(layer_objs) > 0:
active_obj_name = layer_objs[0].name
else:
active_obj_name = ""
layout = self.layout
op = layout.operator(ResampleScalar.bl_idname,
icon=ResampleScalar.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(SignScalar.bl_idname,
icon=SignScalar.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByThreshold.bl_idname,
icon=FillByThreshold.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByRange.bl_idname,
icon=FillByRange.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByLabel.bl_idname,
icon=FillByLabel.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(CombineLabels.bl_idname,
icon=CombineLabels.bl_icon)
op.layer_obj_name = active_obj_name
class RenderSettingMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_RENDER"
bl_label = "Render Setting Presets"
def draw(self, context):
layout = self.layout
is_selected = container_is_selected()
layout.operator(ImportAsScalarLayer.bl_idname,
text=ImportAsScalarLayer.bl_label+" (Add to)"
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
layout.operator(ImportAsLabelLayer.bl_idname,
text=ImportAsLabelLayer.bl_label+" (Add to)"
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
layout.separator()
layout.operator(AddPlaneCutter.bl_idname)
layout.operator(AddCylinderCutter.bl_idname)
layout.operator(AddCubeCutter.bl_idname)
layout.operator(AddSphereCutter.bl_idname)
layout.operator(AddPieCutter.bl_idname)
layout.separator()
layout.operator(PickMesh.bl_idname)
layout.operator(PickVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname)
layout.separator()
layout.operator(SaveCaches.bl_idname)
layout.separator()
layout.operator(SignScalar.bl_idname)
layout.operator(FillByThreshold.bl_idname)
layout.operator(FillByRange.bl_idname)
layout.operator(FillByLabel.bl_idname)
layout.operator(CombineLabels.bl_idname)
def TOPBAR_FILE_IMPORT(self, context):
layout = self.layout
is_selected = container_is_selected()
layout.separator()
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
if is_selected else "Volumetric Data as Bioxel (Init)")
def TOPBAR_FILE_EXPORT(self, context):
layout = self.layout
layout.separator()
layout.operator(ExportVolumetricData.bl_idname,
text="Bioxel Layer (.vdb)")
def VIEW3D_OBJECT(self, context):
layout = self.layout
layout.separator()
layout.menu(BioxelNodesView3DMenu.bl_idname, icon="FILE_VOLUME")
def OUTLINER_OBJECT(self, context):
layout = self.layout
layout.separator()
layout.menu(BioxelNodesOutlinerMenu.bl_idname, icon="FILE_VOLUME")
for k, v in RenderSettingPreset.PRESETS.items():
op = layout.operator(RenderSettingPreset.bl_idname,
text=v)
op.preset = k
class BioxelNodesTopbarMenu(bpy.types.Menu):
@@ -159,39 +124,174 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
def draw(self, context):
layout = self.layout
is_selected = container_is_selected()
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
if is_selected else ImportLayerMenu.bl_label+" (Init)")
layout.separator()
layout.menu(ImportLayerMenu.bl_idname,
icon=ImportLayerMenu.bl_icon)
layout.operator(LoadContainer.bl_idname)
layout.operator(SaveContainer.bl_idname)
layout.separator()
layout.menu(AddCutterMenu.bl_idname)
layout.menu(PickFromContainerMenu.bl_idname)
layout.separator()
layout.operator(SaveStagedData.bl_idname)
layout.operator(ReLinkNodes.bl_idname)
layout.separator()
layout.operator(CleanAllCaches.bl_idname)
layout.separator()
layout.menu(RenderSettingMenu.bl_idname)
def TOPBAR(self, context):
layout = self.layout
layout.menu(BioxelNodesTopbarMenu.bl_idname)
class BioxelNodesNodeMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_NODE"
bl_label = "Bioxel Nodes"
bl_icon = "FILE_VOLUME"
def draw(self, context):
layer_objs = get_selected_objs_in_node_tree(context)
if len(layer_objs) > 0:
active_obj_name = layer_objs[0].name
else:
active_obj_name = ""
layout = self.layout
layout.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon)
layout.operator(RemoveMissingLayers.bl_idname,
icon=RemoveMissingLayers.bl_icon)
layout.separator()
layout.operator(SaveContainer.bl_idname)
layout.menu(AddCutterMenu.bl_idname)
layout.menu(PickFromContainerMenu.bl_idname)
layout.operator(SaveAllLayerCaches.bl_idname,
icon=SaveAllLayerCaches.bl_icon)
layout.separator()
layout.menu(FetchLayerMenu.bl_idname)
op = layout.operator(SaveLayerCache.bl_idname,
icon=SaveLayerCache.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(RenameLayer.bl_idname,
icon=RenameLayer.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(RemoveLayer.bl_idname,
icon=RemoveLayer.bl_icon)
op.layer_obj_name = active_obj_name
layout.separator()
layout.menu(ModifyLayerMenu.bl_idname)
def NODE(self, context):
container_obj = context.object
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
is_container = get_container_obj(container_obj)
if not is_geo_nodes or not is_container:
return
layout = self.layout
layout.separator()
layout.menu(BioxelNodesNodeMenu.bl_idname)
def NODE_PT(self, context):
container_obj = context.object
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
is_container = get_container_obj(container_obj)
self.bl_label = "Group"
if not is_geo_nodes or not is_container:
return
if container_obj.modifiers[0].node_group != context.space_data.edit_tree:
return
self.bl_label = "Bioxel Nodes"
layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
layer_list = layer_list_UL.layer_list
layer_list_active = layer_list_UL.layer_list_active
layer_list.clear()
for layer_obj in get_container_layer_objs(container_obj):
layer_item = layer_list.add()
layer_item.label = get_layer_item_label(context, layer_obj)
layer_item.obj_name = layer_obj.name
layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
for prop in ["kind", "bioxel_size", "shape", "min", "max", ]])
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list):
active_obj_name = layer_list[layer_list_active].obj_name
else:
active_obj_name = ""
layout = self.layout
layout.label(text="Layer List")
split = layout.row()
split.template_list(listtype_name="BIOXELNODES_UL_layer_list",
list_id="layer_list",
dataptr=layer_list_UL,
propname="layer_list",
active_dataptr=layer_list_UL,
active_propname="layer_list_active",
item_dyntip_propname="info_text",
rows=20)
sidebar = split.column(align=True)
sidebar.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon, text="")
sidebar.operator(RemoveMissingLayers.bl_idname,
icon=RemoveMissingLayers.bl_icon, text="")
sidebar.separator()
op = sidebar.operator(FetchLayer.bl_idname,
icon=FetchLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(SaveLayerCache.bl_idname,
icon=SaveLayerCache.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(RenameLayer.bl_idname,
icon=RenameLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(RemoveLayer.bl_idname,
icon=RemoveLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
sidebar.separator()
op = sidebar.operator(ResampleScalar.bl_idname,
icon=ResampleScalar.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(SignScalar.bl_idname,
icon=SignScalar.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByThreshold.bl_idname,
icon=FillByThreshold.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByRange.bl_idname,
icon=FillByRange.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByLabel.bl_idname,
icon=FillByLabel.bl_icon, text="")
op.layer_obj_name = active_obj_name
sidebar.separator()
layout.separator()
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.append(OUTLINER_OBJECT)
bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PT)
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
bpy.types.NODE_MT_editor_menus.append(NODE)
def remove():
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.NODE_PT_node_tree_properties.remove(NODE_PT)
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
bpy.types.NODE_MT_editor_menus.remove(NODE)

View File

@@ -6,32 +6,107 @@ NODE_FILE = "BioxelNodes_4.2"
MENU_ITEMS = [
{
'label': 'Methods',
'label': 'Component',
'icon': 'OUTLINER_DATA_VOLUME',
'items': [
{
'label': 'Mask by Threshold',
'label': 'Cutout by Threshold',
'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_MaskByThreshold',
'node_type': 'BioxelNodes_CutoutByThreshold',
'node_description': ''
},
{
'label': 'Mask by Range',
'label': 'Cutout by Range',
'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_MaskByRange',
'node_type': 'BioxelNodes_CutoutByRange',
'node_description': ''
},
{
'label': 'Mask by Label',
'icon': 'MESH_CAPSULE',
'node_type': 'BioxelNodes_MaskByLabel',
'label': 'Cutout by Color',
'icon': 'COLOR',
'node_type': 'BioxelNodes_CutoutByColor',
'node_description': ''
}
},
"separator",
{
'label': 'To Surface',
'icon': 'MESH_DATA',
'node_type': 'BioxelNodes_ToSurface',
'node_description': ''
},
{
'label': 'Join Component',
'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_JoinComponent',
'node_description': ''
},
]
},
{
'label': 'Shaders',
'icon': 'SHADING_RENDERED',
'label': 'Properties',
'icon': 'PROPERTIES',
'items': [
{
'label': 'Set Properties',
'icon': 'PROPERTIES',
'node_type': 'BioxelNodes_SetProperties',
'node_description': ''
},
"separator",
{
'label': 'Set Color',
'icon': 'IPO_SINE',
'node_type': 'BioxelNodes_SetColor',
'node_description': ''
},
{
'label': 'Set Color by Layer',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByLayer',
'node_description': ''
},
{
'label': 'Set Color by Ramp 2',
'icon': 'IPO_QUAD',
'node_type': 'BioxelNodes_SetColorByRamp2',
'node_description': ''
},
{
'label': 'Set Color by Ramp 3',
'icon': 'IPO_CUBIC',
'node_type': 'BioxelNodes_SetColorByRamp3',
'node_description': ''
},
{
'label': 'Set Color by Ramp 4',
'icon': 'IPO_QUART',
'node_type': 'BioxelNodes_SetColorByRamp4',
'node_description': ''
},
{
'label': 'Set Color by Ramp 5',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByRamp5',
'node_description': ''
},
# "separator",
# {
# 'label': 'Color Presets',
# 'icon': 'COLOR',
# 'node_type': 'BioxelNodes_ColorPresets',
# 'node_description': ''
# }
# {
# 'label': 'Color Presets MRI',
# 'icon': 'COLOR',
# 'node_type': 'BioxelNodes_ColorPresets_MRI',
# 'node_description': ''
# },
]
},
{
'label': 'Surface',
'icon': 'MESH_DATA',
'items': [
{
'label': 'Membrane Shader',
@@ -51,142 +126,6 @@ MENU_ITEMS = [
'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': 'Primitive Cutter',
'icon': 'MOD_LINEART',
'node_type': 'BioxelNodes_PrimitiveCutter',
'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': 'Pie Cutter',
'icon': 'MESH_CONE',
'node_type': 'BioxelNodes_PieObjectCutter',
'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',
@@ -207,6 +146,56 @@ MENU_ITEMS = [
'node_description': ''
}
]
},
{
'label': 'Cutters',
'icon': 'MOD_BEVEL',
'items': [
{
'label': 'Cut',
'icon': 'MOD_BEVEL',
'node_type': 'BioxelNodes_Cut',
'node_description': ''
},
"separator",
{
'label': 'Primitive Cutter',
'icon': 'MOD_LINEART',
'node_type': 'BioxelNodes_PrimitiveCutter',
'node_description': ''
},
{
'label': 'Object Cutter',
'icon': 'MESH_PLANE',
'node_type': 'BioxelNodes_ObjectCutter',
'node_description': ''
}
]
},
{
'label': 'Utils',
'icon': 'MODIFIER',
'items': [
{
'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': ''
}
]
}
]

View File

@@ -1,15 +1,17 @@
import bpy
import bmesh
from ..nodes import custom_nodes
from ..customnodes.nodes import AddCustomNode
from ..bioxel.io import load_container, save_container
from ..bioxelutils.layer import get_all_layer_objs
from ..bioxelutils.container import (container_to_obj, obj_to_container,
get_container_objs_from_selection)
from ..bioxelutils.node import get_nodes_by_type, move_node_between_nodes, move_node_to_node
from .utils import change_render_setting, get_cache_dir, get_preferences, select_object
from ..bioxelutils.container import container_to_obj, obj_to_container
from ..bioxelutils.utils import (get_container_layer_objs, get_container_obj, get_nodes_by_type,
move_node_between_nodes,
move_node_to_node,
get_all_layer_objs)
from .utils import get_cache_dir, select_object
class SaveContainer(bpy.types.Operator):
@@ -23,19 +25,14 @@ class SaveContainer(bpy.types.Operator):
filename_ext = ".bioxel"
@classmethod
def poll(cls, context):
container_objs = get_container_objs_from_selection()
return len(container_objs) > 0
def execute(self, context):
container_objs = get_container_objs_from_selection()
container_obj = get_container_obj(context.object)
if len(container_objs) == 0:
if container_obj is None:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container = obj_to_container(container_objs[0])
container = obj_to_container(container_obj)
save_path = f"{self.filepath.split('.')[0]}.bioxel"
save_container(container, save_path, overwrite=True)
@@ -82,20 +79,13 @@ class LoadContainer(bpy.types.Operator):
class PickObject():
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
container_objs = get_container_objs_from_selection()
return len(container_objs) > 0
def execute(self, context):
container_objs = get_container_objs_from_selection()
container_obj = get_container_obj(context.object)
if len(container_objs) == 0:
if container_obj is None:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container_obj = container_objs[0]
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
obj = bpy.context.active_object
@@ -124,6 +114,7 @@ class PickMesh(bpy.types.Operator, PickObject):
bl_idname = "bioxelnodes.pick_mesh"
bl_label = "Pick Mesh"
bl_description = "Pick Container Mesh"
bl_icon = "OUTLINER_OB_MESH"
object_type = "Mesh"
@@ -131,6 +122,7 @@ class PickVolume(bpy.types.Operator, PickObject):
bl_idname = "bioxelnodes.pick_volume"
bl_label = "Pick Volume"
bl_description = "Pick Container Volume"
bl_icon = "OUTLINER_OB_VOLUME"
object_type = "Volume"
@@ -138,45 +130,34 @@ class PickBboxWire(bpy.types.Operator, PickObject):
bl_idname = "bioxelnodes.pick_bbox_wire"
bl_label = "Pick Bbox Wire"
bl_description = "Pick Container Bbox Wire"
bl_icon = "MESH_CUBE"
object_type = "BboxWire"
class AddCutter():
bl_options = {'UNDO'}
@classmethod
def poll(cls, context):
container_objs = get_container_objs_from_selection()
return len(container_objs) > 0
def execute(self, context):
container_objs = get_container_objs_from_selection()
container_obj = get_container_obj(context.object)
if len(container_objs) == 0:
if container_obj is None:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
container_obj = container_objs[0]
if self.object_type == "plane":
node_type = "BioxelNodes_PlaneObjectCutter"
# TODO: do not use operator to create obj
if self.cutter_type == "plane":
bpy.ops.mesh.primitive_plane_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
elif self.object_type == "cylinder":
node_type = "BioxelNodes_CylinderObjectCutter"
elif self.cutter_type == "cylinder":
bpy.ops.mesh.primitive_cylinder_add(
radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
bpy.context.object.rotation_euler[0] = container_obj.rotation_euler[0]
elif self.object_type == "cube":
node_type = "BioxelNodes_CubeObjectCutter"
elif self.cutter_type == "cube":
bpy.ops.mesh.primitive_cube_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
elif self.object_type == "sphere":
node_type = "BioxelNodes_SphereObjectCutter"
elif self.cutter_type == "sphere":
bpy.ops.mesh.primitive_ico_sphere_add(
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
elif self.object_type == "pie":
node_type = "BioxelNodes_PieObjectCutter"
elif self.cutter_type == "pie":
# Create mesh
pie_mesh = bpy.data.meshes.new('Pie')
@@ -211,6 +192,7 @@ class AddCutter():
bpy.context.view_layer.objects.active = pie
cutter_obj = bpy.context.active_object
cutter_obj.location = container_obj.location
cutter_obj.visible_camera = False
cutter_obj.visible_diffuse = False
cutter_obj.visible_glossy = False
@@ -220,16 +202,23 @@ class AddCutter():
cutter_obj.hide_render = True
cutter_obj.display_type = 'WIRE'
select_object(container_obj)
modifier = container_obj.modifiers[0]
node_group = modifier.node_group
cutter_node = custom_nodes.add_node(node_group, node_type)
cutter_node.inputs[0].default_value = cutter_obj
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')
cutter_node = custom_nodes.add_node(node_group,
"BioxelNodes_ObjectCutter")
cutter_node.inputs[0].default_value = self.cutter_type.capitalize()
cutter_node.inputs[1].default_value = cutter_obj
cut_node = custom_nodes.add_node(node_group,
'BioxelNodes_Cut')
output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 0:
node_group.links.new(cut_node.outputs[0],
output_node.inputs[0])
@@ -247,44 +236,132 @@ class AddCutter():
cut_node.inputs[1])
move_node_to_node(cutter_node, cut_node, (-300, -300))
select_object(cutter_obj)
else:
move_node_to_node(cutter_node, output_node, (0, -100))
select_object(container_obj)
bpy.ops.bioxelnodes.object_cutter('INVOKE_DEFAULT',
cutter_obj_name=cutter_obj.name,
cutter_type=self.cutter_type.capitalize())
return {'FINISHED'}
class ObjectCutter(bpy.types.Operator, AddCustomNode):
bl_idname = "bioxelnodes.object_cutter"
bl_label = "Object Cutter"
bl_description = "Object Cutter"
bl_icon = "NODE"
bl_options = {'UNDO'}
cutter_obj_name: bpy.props.StringProperty() # type: ignore
cutter_type: bpy.props.StringProperty() # type: ignore
def execute(self, context):
cutter_obj = bpy.data.objects.get(self.cutter_obj_name)
if cutter_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
self.get_node_tree(self.node_type, self.node_link)
prev_context = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.ops.node.add_node('INVOKE_DEFAULT',
type='GeometryNodeGroup',
use_transform=True)
bpy.context.area.type = prev_context
node = bpy.context.active_node
self.assign_node_tree(node)
node.show_options = False
node.label = "Object Cutter"
node.inputs[0].default_value = self.cutter_type
node.inputs[1].default_value = cutter_obj
return {"FINISHED"}
def invoke(self, context, event):
self.nodes_file = custom_nodes.nodes_file
self.node_type = "BioxelNodes_ObjectCutter"
return self.execute(context)
class AddPlaneCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_plane_cutter"
bl_label = "Add a Plane Cutter"
bl_description = "Add a Plane Cutter to Container"
object_type = "plane"
bl_icon = "MESH_PLANE"
cutter_type = "plane"
class AddCylinderCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cylinder_cutter"
bl_label = "Add a Cylinder Cutter"
bl_description = "Add a Cylinder Cutter to Container"
object_type = "cylinder"
bl_icon = "MESH_CYLINDER"
cutter_type = "cylinder"
class AddCubeCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cube_cutter"
bl_label = "Add a Cube Cutter"
bl_description = "Add a Cube Cutter to Container"
object_type = "cube"
bl_icon = "MESH_CUBE"
cutter_type = "cube"
class AddSphereCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_sphere_cutter"
bl_label = "Add a Sphere Cutter"
bl_description = "Add a Sphere Cutter to Container"
object_type = "sphere"
bl_icon = "MESH_UVSPHERE"
cutter_type = "sphere"
class AddPieCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_pie_cutter"
bl_label = "Add a Pie Cutter"
bl_description = "Add a Pie Cutter to Container"
object_type = "pie"
bl_icon = "MESH_CONE"
cutter_type = "pie"
class SaveAllLayerCaches(bpy.types.Operator):
bl_idname = "bioxelnodes.save_all_layer_caches"
bl_label = "Save All Layer Caches"
bl_description = "Save Container's caches to directory."
bl_icon = "FILE_TICK"
cache_dir: bpy.props.StringProperty(
name="Cache Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
def execute(self, context):
container_obj = get_container_obj(context.object)
if container_obj is None:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
fails = []
for layer_obj in get_container_layer_objs(container_obj):
try:
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
layer_obj_name=layer_obj.name,
cache_dir=self.cache_dir)
except:
fails.append(layer_obj)
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'}

View File

@@ -5,19 +5,19 @@ import threading
import numpy as np
from pathlib import Path
from ..exceptions import CancelledByUser
from ..props import BIOXELNODES_Series
from ..bioxel.layer import Layer
from ..bioxelutils.layer import (get_all_layer_objs,
get_layer_obj)
from ..bioxelutils.utils import (get_all_layer_objs, get_container_obj,
get_layer_obj,
get_container_objs_from_selection)
from ..bioxelutils.container import (Container,
add_layers,
container_to_obj,
get_container_objs_from_selection)
container_to_obj)
from ..bioxel.layer import Layer
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
get_ext, parse_volumetric_data)
from .utils import (change_render_setting, get_cache_dir, get_preferences,
from .utils import (get_cache_dir,
progress_update, progress_bar, select_object)
# 3rd-party
@@ -78,20 +78,30 @@ class ImportVolumetricData():
return {'RUNNING_MODAL'}
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumetricData):
bl_idname = "bioxelnodes.import_as_scalar_layer"
class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
bl_idname = "bioxelnodes.import_as_scalar"
bl_label = "Import as Scalar"
bl_description = "Import Volumetric Data to Container as Scalar"
bl_icon = "EVENT_S"
read_as = "scalar"
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumetricData):
bl_idname = "bioxelnodes.import_as_label_layer"
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
bl_idname = "bioxelnodes.import_as_label"
bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_L"
read_as = "label"
class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
bl_idname = "bioxelnodes.import_as_color"
bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_C"
read_as = "color"
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
bl_idname = "BIOXELNODES_FH_ImportVolumetricData"
bl_label = "File handler for dicom import"
@@ -100,7 +110,16 @@ class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
@classmethod
def poll_drop(cls, context):
return (context.area and context.area.type == 'VIEW_3D')
if not context.area:
return False
if context.area.type == 'VIEW_3D':
return True
elif context.area.type == 'NODE_EDITOR':
container_obj = get_container_obj(context.object)
return container_obj is not None
else:
return False
def get_series_ids(self, context):
@@ -125,7 +144,6 @@ class ParseVolumetricData(bpy.types.Operator):
meta = None
thread = None
_timer = None
container_obj_name = ""
progress: bpy.props.FloatProperty(name="Progress",
options={"SKIP_SAVE"},
@@ -144,7 +162,8 @@ class ParseVolumetricData(bpy.types.Operator):
read_as: bpy.props.EnumProperty(name="Read as",
default="scalar",
items=[("scalar", "Scalar", ""),
("label", "Labels", "")]) # type: ignore
("label", "Labels", ""),
("color", "Color", "")]) # type: ignore
series_id: bpy.props.EnumProperty(name="Select Series",
items=get_series_ids) # type: ignore
@@ -153,6 +172,10 @@ class ParseVolumetricData(bpy.types.Operator):
type=BIOXELNODES_Series) # type: ignore
def execute(self, context):
if not self.filepath:
self.report({"WARNING"}, "No file selected.")
return {'CANCELLED'}
data_path = Path(self.filepath).resolve()
ext = get_ext(data_path)
if ext not in SUPPORT_EXTS:
@@ -207,7 +230,7 @@ class ParseVolumetricData(bpy.types.Operator):
# Check if user press 'ESC'
if event.type == 'ESC':
self.is_cancelled = True
progress_update(context, 0, "Canceling...")
progress_update(context, 0.0, "Canceling...")
return {'PASS_THROUGH'}
# Check if is the timer time
@@ -227,6 +250,7 @@ class ParseVolumetricData(bpy.types.Operator):
context.window_manager.event_timer_remove(self._timer)
# Remove the progress bar from status bar
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
progress_update(context, 1.0)
# Check if thread is cancelled by user
if self.is_cancelled:
@@ -248,23 +272,32 @@ class ParseVolumetricData(bpy.types.Operator):
orig_shape = self.meta['xyz_shape']
orig_spacing = self.meta['spacing']
if orig_spacing[2] == 1 and orig_spacing[0] < 0.1:
spacing_log10 = math.floor(math.log10(min(*orig_spacing)))
orig_spacing = (orig_spacing[0] * math.pow(10, -spacing_log10-1),
orig_spacing[1] * math.pow(10, -spacing_log10-1),
1)
min_size = min(orig_spacing[0],
orig_spacing[1], orig_spacing[2])
bioxel_size = max(min_size, 1.0)
layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
layer_size = get_layer_size(layer_shape,
bioxel_size)
log10 = math.floor(math.log10(max(*layer_size)))
log10 = max(1, log10)
log10 = min(3, log10)
scene_scale = math.pow(10, -log10)
# layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
# layer_size = get_layer_size(layer_shape,
# bioxel_size)
# log10 = math.floor(math.log10(max(*layer_size)))
# log10 = max(1, log10)
# log10 = min(3, log10)
# scene_scale = math.pow(10, -log10)
scene_scale = 0.01
if self.container_obj_name:
container_obj = bpy.data.objects.get(self.container_obj_name)
if context.area.type == "NODE_EDITOR":
container_obj = context.object
container_name = container_obj.name
container_obj_name = container_name
else:
container_name = self.meta['name']
container_obj_name = ""
series_id = self.series_id if self.series_id != "empty" else ""
bpy.ops.bioxelnodes.import_volumetric_data_dialog(
@@ -278,7 +311,7 @@ class ParseVolumetricData(bpy.types.Operator):
series_id=series_id,
frame_count=self.meta['frame_count'],
channel_count=self.meta['channel_count'],
container_obj_name=self.container_obj_name,
container_obj_name=container_obj_name,
read_as=self.read_as,
scene_scale=scene_scale
)
@@ -287,18 +320,20 @@ class ParseVolumetricData(bpy.types.Operator):
return {'FINISHED'}
def invoke(self, context, event):
if not self.filepath:
if context.window_manager.bioxelnodes_progress_factor < 1:
print("A process is executing, please wait for it to finish.")
return {'CANCELLED'}
if not self.filepath:
return self.execute(context)
data_path = Path(self.filepath).resolve()
ext = get_ext(data_path)
container_objs = get_container_objs_from_selection()
if len(container_objs) > 0:
self.container_obj_name = container_objs[0].name
title = f"Add to **{self.container_obj_name}**" \
if self.container_obj_name != "" else f"Init a Container"
if context.area.type == "NODE_EDITOR":
title = f"Add to **{context.object.name}**"
else:
title = "Init a Container"
# Series Selection
if ext in DICOM_EXTS:
@@ -367,8 +402,7 @@ class ParseVolumetricData(bpy.types.Operator):
return {'CANCELLED'}
if self.skip_read_as:
self.execute(context)
return {'RUNNING_MODAL'}
return self.execute(context)
else:
context.window_manager.invoke_props_dialog(self,
width=400,
@@ -429,7 +463,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
read_as: bpy.props.EnumProperty(name="Read as",
default="scalar",
items=[("scalar", "Scalar", ""),
("label", "Labels", "")]) # type: ignore
("label", "Labels", ""),
("color", "Color", "")]) # type: ignore
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
soft_min=0.1, soft_max=10.0,
@@ -540,12 +575,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
try:
layer = Layer(data=data == np.full_like(data, i+1),
name=name_i,
kind=kind,
affine=affine)
kind=kind)
layer.resize(shape=shape,
progress_callback=progress_callback)
layer.affine = affine
layers.append(layer)
except CancelledByUser:
return
@@ -553,6 +589,64 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
self.has_error = e
return
if kind == "color":
if np.issubdtype(np.uint8, data.dtype):
data = np.multiply(data, 1.0 / 256,
dtype=np.float32)
elif data.dtype.kind in ['u', 'i']:
min_val = data.min()
max_val = data.max()
# Avoid division by zero if all values are the same
if max_val != min_val:
# Normalize the array to the range (0,1)
data = (data - min_val) / (max_val - min_val)
else:
# If all values are the same, the normalized array will be all zeros
data = np.zeros_like(data, dtype=np.float32)
# Convert the normalized array to float dtype
data = data.astype(np.float32)
else:
data = data.astype(np.float32)
name = self.layer_name or "Color"
if data.shape[4] == 1:
data = np.repeat(data, repeats=3, axis=4)
elif data.shape[4] == 2:
d_shape = list(data.shape)
d_shape = d_shape[:4] + [1]
zore = np.zeros(tuple(d_shape), dtype=np.float32)
data = np.concatenate((data, zore), axis=-1)
elif data.shape[4] > 3:
data = data[:, :, :, :, :3]
if cancel():
return
progress_update(context, 0.2,
f"Processing {name}...")
progress_callback = progress_callback_factory(name,
0.2,
0.7)
try:
layer = Layer(data=data,
name=name,
kind=kind)
layer.resize(shape=shape,
progress_callback=progress_callback)
layer.affine = affine
layers.append(layer)
except CancelledByUser:
return
except Exception as e:
self.has_error = e
return
elif kind == "scalar":
name = self.layer_name or "Scalar"
@@ -573,12 +667,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
try:
layer = Layer(data=data[:, :, :, :, i:i+1],
name=name_i,
kind=kind,
affine=affine)
kind=kind)
layer.resize(shape=shape,
progress_callback=progress_callback)
layer.affine = affine
layers.append(layer)
except CancelledByUser:
return
@@ -598,12 +693,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
try:
layer = Layer(data=data,
name=name,
kind=kind,
affine=affine)
kind=kind)
layer.resize(shape=shape,
progress_callback=progress_callback)
layer.affine = affine
layers.append(layer)
except CancelledByUser:
return
@@ -634,7 +730,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
def modal(self, context, event):
if event.type == 'ESC':
self.is_cancelled = True
progress_update(context, 0, "Canceling...")
progress_update(context, 0.0, "Canceling...")
return {'PASS_THROUGH'}
if event.type != 'TIMER':
@@ -647,6 +743,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
self.thread.join()
context.window_manager.event_timer_remove(self._timer)
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
progress_update(context, 1.0)
if self.is_cancelled:
self.report({"WARNING"}, "Canncelled by user.")
@@ -672,7 +769,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
container_obj=container_obj,
cache_dir=get_cache_dir(context))
else:
container = Container(name=self.container_name,
name = self.container_name or "Container"
container = Container(name=name,
layers=self.layers)
container_obj = container_to_obj(container,
@@ -683,7 +781,10 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
# Change render setting for better result
if is_first_import:
change_render_setting(context)
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
preset="slice_viewer")
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
preset="cycles_preview")
self.report({"INFO"}, "Successfully Imported")
return {'FINISHED'}

View File

@@ -1,23 +1,19 @@
from pathlib import Path
import bpy
import re
import numpy as np
from ..bioxel.layer import Layer
from ..bioxelutils.node import get_nodes_by_type
from ..bioxelutils.container import add_layers, get_container_obj
from ..bioxelutils.layer import obj_to_layer, get_container_layer_objs, get_layer_obj
from .utils import get_preferences, select_object
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
try:
node_group = layer_obj.modifiers[0].node_group
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
return layer_node.inputs[prop].default_value
except:
return None
from ..utils import copy_to_dir
from ..customnodes.nodes import AddCustomNode
from ..bioxelutils.utils import (get_container_obj,
get_layer_prop_value,
get_container_layer_objs,
get_node_type, set_layer_prop_value)
from ..bioxelutils.layer import layer_to_obj, obj_to_layer
from .utils import get_cache_dir, get_layer_item_label, get_layer_label, get_preferences
from ..nodes import custom_nodes
def get_label_layer_selection(self, context):
@@ -33,78 +29,203 @@ def get_label_layer_selection(self, context):
return items
def get_selected_objs_in_node_tree(context):
select_objs = []
# node_group = context.space_data.edit_tree
for node in context.selected_nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
layer_obj = node.inputs[0].default_value
if layer_obj != None:
cache_filepath = Path(bpy.path.abspath(
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
select_objs.append(layer_obj)
return select_objs
class LayerOperator():
bl_options = {'UNDO'}
base_obj: bpy.types.Object = None
layer_obj_name: bpy.props.StringProperty(
options={"HIDDEN"}) # type: ignore
def add_layer(self, context, layer):
preferences = get_preferences(context)
cache_dir = Path(preferences.cache_dir, 'VDBs')
container_obj = self.base_obj.parent
@property
def layer_obj(self):
return bpy.data.objects.get(self.layer_obj_name)
add_layers([layer],
container_obj,
cache_dir)
@property
def is_lost(self):
if self.layer_obj is None:
return None
select_object(container_obj)
cache_filepath = Path(bpy.path.abspath(
self.layer_obj.data.filepath)).resolve()
return not cache_filepath.is_file()
class FetchLayer(bpy.types.Operator, LayerOperator, AddCustomNode):
bl_idname = "bioxelnodes.fetch_layer"
bl_label = "Fetch Layer"
bl_description = "Fetch Layer"
bl_icon = "NODE"
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
self.get_node_tree(self.node_type, self.node_link)
prev_context = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.ops.node.add_node('INVOKE_DEFAULT',
type='GeometryNodeGroup',
use_transform=True)
bpy.context.area.type = prev_context
node = bpy.context.active_node
self.assign_node_tree(node)
node.show_options = False
layer_obj = self.layer_obj
node.inputs[0].default_value = layer_obj
node.label = get_layer_label(layer_obj)
return {"FINISHED"}
def invoke(self, context, event):
self.base_obj = get_layer_obj(bpy.context.active_object)
self.nodes_file = custom_nodes.nodes_file
self.node_type = "BioxelNodes_FetchLayer"
return self.execute(context)
class ScalarOperator(LayerOperator):
@classmethod
def poll(cls, context):
label_objs = [obj for obj in context.selected_ids
if get_layer_prop_value(obj, "kind") == "scalar"]
return True if len(label_objs) > 0 else False
class FetchLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_ADD_LAYER"
bl_label = "Fetch Layer"
def draw(self, context):
container_obj = get_container_obj(bpy.context.active_object)
layer_objs = get_container_layer_objs(container_obj)
layout = self.layout
for layer_obj in layer_objs:
op = layout.operator(FetchLayer.bl_idname,
text=get_layer_item_label(context, layer_obj))
op.layer_obj_name = layer_obj.name
class LabelOperator(LayerOperator):
@classmethod
def poll(cls, context):
label_objs = [obj for obj in context.selected_ids
if get_layer_prop_value(obj, "kind") == "label"]
return True if len(label_objs) > 0 else False
class ModifyLayerOperator(LayerOperator):
def layer_operate(self, orig_layer: Layer):
"""do the operation"""
return orig_layer
def add_layer_node(self, context, layer):
layer_obj = layer_to_obj(layer,
container_obj=self.layer_obj.parent,
cache_dir=get_cache_dir(context))
class SignScalar(bpy.types.Operator, ScalarOperator):
bl_idname = "bioxelnodes.sign_scalar"
bl_label = "Sign Scalar"
bl_description = "Sign the scalar value"
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
layer_obj_name=layer_obj.name)
def execute(self, context):
base_layer = obj_to_layer(self.base_obj)
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
modified_layer = base_layer.copy()
modified_layer.data = -base_layer.data
modified_layer.name = f"{base_layer.name}_Signed"
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
self.add_layer(context, modified_layer)
orig_layer = obj_to_layer(self.layer_obj)
new_layer = self.layer_operate(orig_layer)
self.add_layer_node(context, new_layer)
return {'FINISHED'}
class FillOperator(ScalarOperator):
def invoke(self, context, event):
self.base_obj = get_layer_obj(bpy.context.active_object)
scalar_min = get_layer_prop_value(self.base_obj, "min")
self.fill_value = min(scalar_min, 0)
context.window_manager.invoke_props_dialog(self, width=400)
return {'RUNNING_MODAL'}
class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
bl_idname = "bioxelnodes.resample_scalar"
bl_label = "Resample Scalar"
bl_description = "Resample Scalar"
bl_icon = "ALIASED"
class FillByThreshold(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_threshold"
bl_label = "Fill Value by Threshold"
bl_description = "Fill Value by Threshold"
threshold: bpy.props.FloatProperty(
name="Threshold",
soft_min=0, soft_max=1024,
default=128,
bioxel_size: bpy.props.FloatProperty(
name="Bioxel Size",
soft_min=0.1, soft_max=10.0,
default=1,
) # type: ignore
@staticmethod
def get_new_shape(orig_shape, orig_size, new_size):
return (int(orig_shape[0]*orig_size/new_size),
int(orig_shape[1]*orig_size/new_size),
int(orig_shape[2]*orig_size/new_size))
def layer_operate(self, orig_layer: Layer):
modified_layer = orig_layer.copy()
new_shape = self.get_new_shape(modified_layer.shape,
modified_layer.bioxel_size[0],
self.bioxel_size)
modified_layer.resize(new_shape)
modified_layer.name = f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
return modified_layer
def draw(self, context):
orig_shape = get_layer_prop_value(self.layer_obj, "shape")
orig_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
new_shape = self.get_new_shape(orig_shape,
orig_size,
self.bioxel_size)
orig_shape = tuple(orig_shape)
bioxel_count = new_shape[0] * new_shape[1] * new_shape[2]
layer_shape_text = f"Shape from {str(orig_shape)} to {str(new_shape)}"
if bioxel_count > 100000000:
layer_shape_text += "**TOO LARGE!**"
layout = self.layout
layout.prop(self, "bioxel_size")
layout.label(text=layer_shape_text)
def invoke(self, context, event):
if self.layer_obj:
bioxel_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
self.bioxel_size = bioxel_size
context.window_manager.invoke_props_dialog(self,
width=400,
title=f"Resample {self.layer_obj.name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class SignScalar(bpy.types.Operator, ModifyLayerOperator):
bl_idname = "bioxelnodes.sign_scalar"
bl_label = "Sign Scalar"
bl_description = "Sign the scalar value"
bl_icon = "REMOVE"
def layer_operate(self, orig_layer: Layer):
modified_layer = orig_layer.copy()
modified_layer.data = -orig_layer.data
modified_layer.name = f"{orig_layer.name}_Sign"
return modified_layer
def invoke(self, context, event):
if self.layer_obj:
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to sign {self.layer_obj.name}?")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class FillOperator(ModifyLayerOperator):
fill_value: bpy.props.FloatProperty(
name="Fill Value",
soft_min=0, soft_max=1024.0,
@@ -112,30 +233,51 @@ class FillByThreshold(bpy.types.Operator, FillOperator):
) # type: ignore
invert: bpy.props.BoolProperty(
name="Invert Area",
name="Invert Aera",
default=True,
) # type: ignore
def execute(self, context):
base_layer = obj_to_layer(self.base_obj)
def invoke(self, context, event):
if self.layer_obj:
scalar_min = get_layer_prop_value(self.layer_obj, "min")
data = np.amax(base_layer.data, -1)
self.fill_value = min(scalar_min, 0)
context.window_manager.invoke_props_dialog(self,
width=400,
title=f"Fill {self.layer_obj.name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class FillByThreshold(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_threshold"
bl_label = "Fill Value by Threshold"
bl_description = "Fill Value by Threshold"
bl_icon = "EMPTY_SINGLE_ARROW"
threshold: bpy.props.FloatProperty(
name="Threshold",
soft_min=0, soft_max=1024,
default=128,
) # type: ignore
def layer_operate(self, orig_layer: Layer):
data = np.amax(orig_layer.data, -1)
mask = data <= self.threshold \
if self.invert else data > self.threshold
modified_layer = base_layer.copy()
modified_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{base_layer.name}_{self.threshold}-Filled"
self.add_layer(context, modified_layer)
return {'FINISHED'}
modified_layer.name = f"{orig_layer.name}_F-{self.threshold}"
return modified_layer
class FillByRange(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_range"
bl_label = "Fill Value by Range"
bl_description = "Fill Value by Range"
bl_icon = "IPO_CONSTANT"
from_min: bpy.props.FloatProperty(
name="From Min",
@@ -149,82 +291,52 @@ class FillByRange(bpy.types.Operator, FillOperator):
default=256,
) # type: ignore
fill_value: bpy.props.FloatProperty(
name="Fill Value",
soft_min=0, soft_max=1024.0,
default=0,
) # type: ignore
invert: bpy.props.BoolProperty(
name="Invert Area",
default=True,
) # type: ignore
def execute(self, context):
base_layer = obj_to_layer(self.base_obj)
data = np.amax(base_layer.data, -1)
def layer_operate(self, orig_layer: Layer):
data = np.amax(orig_layer.data, -1)
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
(data > self.from_min) & (data < self.from_max)
modified_layer = base_layer.copy()
modified_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{base_layer.name}_{self.from_min}-{self.from_max}-Filled"
self.add_layer(context, modified_layer)
return {'FINISHED'}
modified_layer.name = f"{orig_layer.name}_F-{self.from_min}-{self.from_max}"
return modified_layer
class FillByLabel(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_label"
bl_label = "Fill Value by Label"
bl_description = "Fill Value by Label Area"
bl_icon = "MESH_CAPSULE"
label_obj_name: bpy.props.EnumProperty(
name="Label Layer",
items=get_label_layer_selection
) # type: ignore
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
items=get_label_layer_selection) # type: ignore
fill_value: bpy.props.FloatProperty(
name="Fill Value",
soft_min=0, soft_max=1024.0,
default=0,
) # type: ignore
invert: bpy.props.BoolProperty(
name="Invert Label",
default=True,
) # type: ignore
def execute(self, context):
def layer_operate(self, orig_layer: Layer):
label_obj = bpy.data.objects.get(self.label_obj_name)
if not label_obj:
self.report({"WARNING"}, "Cannot find any label layer.")
return {'FINISHED'}
base_layer = obj_to_layer(self.base_obj)
label_layer = obj_to_layer(label_obj)
label_layer.resize(base_layer.shape)
label_layer.resize(orig_layer.shape)
mask = np.amax(label_layer.data, -1)
if self.invert:
mask = 1 - mask
modified_layer = base_layer.copy()
modified_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{base_layer.name}_{label_layer.name}-Filled"
self.add_layer(context, modified_layer)
return {'FINISHED'}
modified_layer.name = f"{orig_layer.name}_F-{label_layer.name}"
return modified_layer
class CombineLabels(bpy.types.Operator, LabelOperator):
class CombineLabels(bpy.types.Operator, ModifyLayerOperator):
bl_idname = "bioxelnodes.combine_labels"
bl_label = "Combine Labels"
bl_description = "Combine all selected labels"
bl_icon = "MOD_BUILD"
def execute(self, context):
label_objs = [obj for obj in context.selected_ids
label_objs = [obj for obj in get_selected_objs_in_node_tree(context)
if get_layer_prop_value(obj, "kind") == "label"]
if len(label_objs) < 2:
@@ -240,11 +352,173 @@ class CombineLabels(bpy.types.Operator, LabelOperator):
for label_obj in label_objs:
label_layer = obj_to_layer(label_obj)
label_layer.resize(base_layer.shape)
modified_layer.data = np.maximum(modified_layer.data, label_layer.data)
modified_layer.data = np.maximum(
modified_layer.data, label_layer.data)
label_names.append(label_layer.name)
modified_layer.name = f"{'-'.join(label_names)}-Combined"
modified_layer.name = f"C-{'-'.join(label_names)}"
self.add_layer(context, modified_layer)
self.add_layer_node(context, modified_layer)
return {'FINISHED'}
class SaveLayerCache(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.save_layer_cache"
bl_label = "Save Layer Cache"
bl_description = "Save Layer Cache"
bl_icon = "FILE_TICK"
cache_dir: bpy.props.StringProperty(
name="Cache Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
# "//"
output_dir = bpy.path.abspath(self.cache_dir)
source_dir = bpy.path.abspath(self.layer_obj.data.filepath)
source_path: Path = Path(source_dir).resolve()
is_sequence = self.layer_obj.data.is_sequence
name = self.layer_obj.name if is_sequence else f"{self.layer_obj.name}.vdb"
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
if is_sequence else Path(output_dir, name).resolve()
if output_path != source_path:
copy_to_dir(source_path.parent if is_sequence else source_path,
output_path.parent.parent if is_sequence else output_path.parent,
new_name=name)
blend_path = Path(bpy.path.abspath("//")).resolve()
self.layer_obj.data.filepath = bpy.path.relpath(str(output_path),
start=str(blend_path))
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RenameLayer(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.rename_layer"
bl_label = "Rename Layer"
bl_description = "Rename Layer"
bl_icon = "FILE_FONT"
name: bpy.props.StringProperty(name="New Name") # type: ignore
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
name = f"{self.layer_obj.parent.name}_{self.name}"
self.layer_obj.name = name
self.layer_obj_name = name
self.layer_obj.data.name = name
set_layer_prop_value(self.layer_obj, "name", self.name)
node_group = context.space_data.edit_tree
for node in node_group.nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == self.layer_obj:
node.label = self.name
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
self.name = get_layer_prop_value(self.layer_obj, "name")
context.window_manager.invoke_props_dialog(self,
width=500,
title=f"rename {self.layer_obj.name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RemoveLayer(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.remove_layer"
bl_label = "Remove Selected Layer"
bl_description = "Remove Layer"
bl_icon = "TRASH"
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
node_group = context.space_data.edit_tree
for node in node_group.nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == self.layer_obj:
node_group.nodes.remove(node)
cache_filepath = Path(bpy.path.abspath(
self.layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
if self.layer_obj.data.is_sequence:
for f in cache_filepath.parent.iterdir():
f.unlink(missing_ok=True)
else:
cache_filepath.unlink(missing_ok=True)
# also remove layer object
bpy.data.volumes.remove(self.layer_obj.data)
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to remove {self.layer_obj.name}?")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RemoveMissingLayers(bpy.types.Operator):
bl_idname = "bioxelnodes.remove_lost_layers"
bl_label = "Remove All Missing Layers"
bl_description = "Remove all missing "
bl_icon = "BRUSH_DATA"
def execute(self, context):
container_obj = context.object
for layer_obj in get_container_layer_objs(container_obj):
cache_filepath = Path(bpy.path.abspath(
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
continue
bpy.ops.bioxelnodes.remove_layer('EXEC_DEFAULT',
layer_obj_name=layer_obj.name)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to remove all **Missing** layers?")
return {'RUNNING_MODAL'}

View File

@@ -1,14 +1,11 @@
import re
import bpy
from pathlib import Path
import shutil
from ..utils import copy_to_dir
from .utils import get_cache_dir
from ..nodes import custom_nodes
from ..bioxelutils.container import get_container_objs_from_selection
from ..bioxelutils.layer import get_all_layer_objs, get_container_layer_objs
from ..bioxelutils.utils import (get_container_objs_from_selection,
get_all_layer_objs,
get_container_layer_objs)
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
@@ -32,30 +29,6 @@ class ReLinkNodes(bpy.types.Operator):
return {'FINISHED'}
def save_layer_cache(layer_obj, output_dir):
pattern = r'\.\d{4}\.'
# "//"
output_dir = bpy.path.abspath(output_dir)
source_dir = bpy.path.abspath(layer_obj.data.filepath)
source_path: Path = Path(source_dir).resolve()
is_sequence = re.search(pattern, source_path.name) is not None
name = layer_obj.name if is_sequence else f"{layer_obj.name}.vdb"
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
if is_sequence else Path(output_dir, name).resolve()
if output_path != source_path:
copy_to_dir(source_path.parent if is_sequence else source_path,
output_path.parent.parent if is_sequence else output_path.parent,
new_name=name)
blend_path = Path(bpy.path.abspath("//")).resolve()
layer_obj.data.filepath = bpy.path.relpath(
str(output_path), start=str(blend_path))
class SaveStagedData(bpy.types.Operator):
bl_idname = "bioxelnodes.save_staged_data"
bl_label = "Save Staged Data"
@@ -114,17 +87,19 @@ class SaveStagedData(bpy.types.Operator):
if self.save_cache:
fails = []
for layer in get_all_layer_objs():
for layer_obj in get_all_layer_objs():
try:
save_layer_cache(layer, self.cache_dir)
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
layer_obj_name=layer_obj.name,
cache_dir=self.cache_dir)
except:
fails.append(layer)
fails.append(layer_obj)
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} fail to save.")
return {'FINISHED'}
@@ -147,51 +122,6 @@ class SaveStagedData(bpy.types.Operator):
panel.prop(self, "lib_dir")
class SaveCaches(bpy.types.Operator):
bl_idname = "bioxelnodes.save_caches"
bl_label = "Save Caches"
bl_description = "Save Container's caches to directory."
cache_dir: bpy.props.StringProperty(
name="Layer Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
@classmethod
def poll(cls, context):
container_objs = get_container_objs_from_selection()
return len(container_objs) > 0
def execute(self, context):
container_objs = get_container_objs_from_selection()
if len(container_objs) == 0:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
fails = []
for container_obj in container_objs:
for layer_obj in get_container_layer_objs(container_obj):
try:
save_layer_cache(layer_obj, self.cache_dir)
except:
fails.append(layer_obj)
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'}
class CleanAllCaches(bpy.types.Operator):
bl_idname = "bioxelnodes.clear_all_caches"
bl_label = "Clean All Caches in Temp"
@@ -213,3 +143,71 @@ class CleanAllCaches(bpy.types.Operator):
event,
message="All caches will be cleaned, include other project files, do you still want to clean?")
return {'RUNNING_MODAL'}
class RenderSettingPreset(bpy.types.Operator):
bl_idname = "bioxelnodes.render_setting_preset"
bl_label = "Render Setting Preset"
bl_description = "Render Setting Preset"
PRESETS = {
"slice_viewer": "Slice Viewer",
"eevee_preview": "EEVEE Preview",
"eevee_production": "EEVEE Production",
"cycles_preview": "Cycles Preview",
"cycles_production": "Cycles Production"
}
preset: bpy.props.EnumProperty(name="Preset",
default="eevee_preview",
items=[(k, v, "")
for k, v in PRESETS.items()]) # type: ignore
def execute(self, context):
if self.preset == "eevee_preview":
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
bpy.context.scene.eevee.use_taa_reprojection = False
bpy.context.scene.eevee.taa_samples = 16
bpy.context.scene.eevee.volumetric_tile_size = '2'
bpy.context.scene.eevee.volumetric_shadow_samples = 128
bpy.context.scene.eevee.volumetric_samples = 128
bpy.context.scene.eevee.volumetric_ray_depth = 16
bpy.context.scene.eevee.use_volumetric_shadows = True
elif self.preset == "eevee_production":
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
bpy.context.scene.eevee.use_taa_reprojection = False
bpy.context.scene.eevee.taa_samples = 16
bpy.context.scene.eevee.volumetric_tile_size = '1'
bpy.context.scene.eevee.volumetric_shadow_samples = 128
bpy.context.scene.eevee.volumetric_samples = 256
bpy.context.scene.eevee.volumetric_ray_depth = 16
bpy.context.scene.eevee.use_volumetric_shadows = True
elif self.preset == "cycles_preview":
bpy.context.scene.render.engine = 'CYCLES'
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
bpy.context.scene.cycles.volume_step_rate = 10
elif self.preset == "cycles_production":
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.shading_system = True
bpy.context.scene.cycles.volume_bounces = 16
bpy.context.scene.cycles.transparent_max_bounces = 32
bpy.context.scene.cycles.volume_preview_step_rate = 1
bpy.context.scene.cycles.volume_step_rate = 1
elif self.preset == "slice_viewer":
# bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
bpy.context.scene.eevee.use_taa_reprojection = False
bpy.context.scene.eevee.taa_samples = 4
bpy.context.scene.eevee.volumetric_tile_size = '2'
bpy.context.scene.eevee.volumetric_shadow_samples = 128
bpy.context.scene.eevee.volumetric_samples = 128
bpy.context.scene.eevee.volumetric_ray_depth = 1
bpy.context.scene.eevee.use_volumetric_shadows = False
return {'FINISHED'}

View File

@@ -1,31 +1,10 @@
import bpy
from pathlib import Path
from ..bioxelutils.utils import get_layer_prop_value
from .. import __package__ as base_package
def change_render_setting(context):
preferences = get_preferences(context)
if preferences.do_change_render_setting:
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
bpy.context.scene.cycles.volume_step_rate = 10
except:
pass
try:
bpy.context.scene.eevee.use_taa_reprojection = False
bpy.context.scene.eevee.volumetric_tile_size = '2'
bpy.context.scene.eevee.volumetric_shadow_samples = 128
bpy.context.scene.eevee.volumetric_samples = 256
bpy.context.scene.eevee.use_volumetric_shadows = True
except:
pass
def select_object(target_obj):
for obj in bpy.data.objects:
obj.select_set(False)
@@ -58,3 +37,24 @@ def get_cache_dir(context):
cache_path = Path(preferences.cache_dir, 'VDBs')
cache_path.mkdir(parents=True, exist_ok=True)
return str(cache_path)
def get_layer_item_label(context, layer_obj):
label = get_layer_label(layer_obj)
cache_filepath = Path(bpy.path.abspath(layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
cache_dirpath = Path(get_cache_dir(context))
if cache_dirpath in cache_filepath.parents:
label = "* " + label
else:
label = "**MISSING**" + label
return label
def get_layer_label(layer_obj):
name = get_layer_prop_value(layer_obj, "name")
kind = get_layer_prop_value(layer_obj, "kind")
return f"{name}"

View File

@@ -1,5 +1,42 @@
import bpy
from .bioxelutils.utils import get_node_type
class BIOXELNODES_UL_layer_list(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
layout.label(text=item.label, translate=False, icon_value=icon)
class BIOXELNODES_Series(bpy.types.PropertyGroup):
id: bpy.props.StringProperty() # type: ignore
label: bpy.props.StringProperty() # type: ignore
def select_layer(self, context):
layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
layer_list = layer_list_UL.layer_list
layer_list_active = layer_list_UL.layer_list_active
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list):
layer_obj = bpy.data.objects[layer_list[layer_list_active].obj_name]
node_group = context.space_data.edit_tree
for node in node_group.nodes:
node.select = False
if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == layer_obj:
node.select = True
class BIOXELNODES_Layer(bpy.types.PropertyGroup):
obj_name: bpy.props.StringProperty() # type: ignore
label: bpy.props.StringProperty() # type: ignore
info_text: bpy.props.StringProperty() # type: ignore
class BIOXELNODES_LayerListUL(bpy.types.PropertyGroup):
layer_list: bpy.props.CollectionProperty(
type=BIOXELNODES_Layer) # type: ignore
layer_list_active: bpy.props.IntProperty(default=-1,
update=select_layer,
options=set()) # type: ignore

View File

@@ -16,7 +16,8 @@ required_packages = ["SimpleITK==2.3.1",
"pyometiff==1.0.0",
"mrcfile==1.5.1",
"h5py==3.11.0",
"transforms3d==0.4.2"]
"transforms3d==0.4.2",
"tifffile==2024.7.24"]
platforms = {"windows-x64": Platform(pypi_suffix="win_amd64",

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "bioxelnodes"
version = "0.3.3"
version = "0.4.0"
description = ""
authors = ["Ma Nan <icrdr2010@outlook.com>"]
license = "MIT"