2
.gitignore
vendored
2
.gitignore
vendored
@@ -133,4 +133,6 @@ blendcache_*
|
||||
|
||||
.secrets
|
||||
|
||||
.vdb
|
||||
|
||||
!scipy_ndimage/*/**
|
||||
@@ -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()
|
||||
|
||||
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.2.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.2.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
Binary file not shown.
@@ -2,12 +2,10 @@ import copy
|
||||
import numpy as np
|
||||
|
||||
from . import scipy
|
||||
from . import skimage as ski
|
||||
from . import scipy as ndi
|
||||
|
||||
# 3rd-party
|
||||
import transforms3d
|
||||
|
||||
# TODO: turn to dataclasses
|
||||
|
||||
|
||||
@@ -71,23 +69,27 @@ class Layer():
|
||||
def copy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def fill(self, value: float, mask: np.ndarray):
|
||||
def fill(self, value: float, mask: np.ndarray, smooth: int = 0):
|
||||
mask_frames = ()
|
||||
if mask.ndim == 4:
|
||||
if mask.shape[0] != self.frame_count:
|
||||
raise Exception("Mask frame count is not same as ")
|
||||
for f in range(self.frame_count):
|
||||
mask_frame = mask[f, :, :, :]
|
||||
mask_frame = scipy.minimum_filter(
|
||||
mask_frame.astype(np.float32), size=3)
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
elif mask.ndim == 3:
|
||||
for f in range(self.frame_count):
|
||||
mask_frame = mask[:, :, :]
|
||||
mask_frame = scipy.minimum_filter(
|
||||
mask_frame.astype(np.float32), size=3)
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
@@ -98,20 +100,19 @@ class Layer():
|
||||
_mask = np.expand_dims(_mask, axis=-1)
|
||||
self.data = _mask * value + (1-_mask) * self.data
|
||||
|
||||
def resize(self, shape: tuple, progress_callback=None):
|
||||
def resize(self, shape: tuple, smooth: int = 0, progress_callback=None):
|
||||
if len(shape) != 3:
|
||||
raise Exception("Shape must be 3 dim")
|
||||
|
||||
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):
|
||||
@@ -122,13 +123,22 @@ class Layer():
|
||||
# shape,
|
||||
# preserve_range=True,
|
||||
# anti_aliasing=data.dtype.kind != "b")
|
||||
frame = data[f, :, :, :, :]
|
||||
if smooth > 0:
|
||||
frame = scipy.median_filter(frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
|
||||
factors = np.divide(self.shape, shape)
|
||||
zoom_factors = [1 / f for f in factors]
|
||||
frame = ndi.zoom(data[f, :, :, :],
|
||||
zoom_factors,
|
||||
order = 0 if frame.dtype == bool else 1
|
||||
frame = ndi.zoom(frame,
|
||||
zoom_factors+[1.0],
|
||||
mode="nearest",
|
||||
grid_mode=False,
|
||||
order=order)
|
||||
|
||||
if smooth > 0:
|
||||
frame = frame.astype(self.dtype)
|
||||
data_frames += (frame,)
|
||||
|
||||
data = np.stack(data_frames)
|
||||
@@ -137,7 +147,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)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from .layer import Layer
|
||||
|
||||
# 3rd-party
|
||||
import SimpleITK as sitk
|
||||
@@ -42,7 +41,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 +64,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 +106,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 +140,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,7 +159,13 @@ def collect_sequence(filepath: Path):
|
||||
return sequence
|
||||
|
||||
|
||||
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None) -> Layer:
|
||||
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):
|
||||
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
||||
|
||||
Args:
|
||||
@@ -139,7 +180,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 +222,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 +286,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 +338,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)}")
|
||||
|
||||
245
bioxelnodes/bioxelutils/common.py
Normal file
245
bioxelnodes/bioxelutils/common.py
Normal file
@@ -0,0 +1,245 @@
|
||||
from ast import literal_eval
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
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_name: str):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
prop = layer_node.inputs.get(prop_name)
|
||||
if prop is None:
|
||||
return None
|
||||
|
||||
value = 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)
|
||||
elif type(value).__name__ == "float":
|
||||
value = float(value)
|
||||
return round(value, 2) \
|
||||
if prop in ["bioxel_size"] else value
|
||||
elif type(value).__name__ == "int":
|
||||
value = int(value)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def get_layer_name(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "name")
|
||||
|
||||
|
||||
def get_layer_kind(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "kind")
|
||||
|
||||
|
||||
def get_layer_label(layer_obj):
|
||||
name = get_layer_name(layer_obj)
|
||||
# kind = get_layer_kind(layer_obj)
|
||||
|
||||
label = f"{name}"
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
return "**MISSING**" + label
|
||||
elif is_temp_layer(layer_obj):
|
||||
return "* " + label
|
||||
else:
|
||||
return label
|
||||
|
||||
|
||||
def is_missing_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
return not cache_filepath.is_file()
|
||||
|
||||
|
||||
def is_temp_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
cache_dirpath = Path(get_cache_dir())
|
||||
return cache_dirpath in cache_filepath.parents
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def add_driver(target, target_prop, var_sources, expression):
|
||||
driver = target.driver_add(target_prop)
|
||||
is_vector = isinstance(driver, list)
|
||||
drivers = driver if is_vector else [driver]
|
||||
|
||||
for i, driver in enumerate(drivers):
|
||||
for j, var_source in enumerate(var_sources):
|
||||
|
||||
source = var_source['source']
|
||||
prop = var_source['prop']
|
||||
|
||||
var = driver.driver.variables.new()
|
||||
var.name = f"var{j}"
|
||||
|
||||
var.targets[0].id_type = source.id_type
|
||||
var.targets[0].id = source
|
||||
var.targets[0].data_path = f'["{prop}"][{i}]'\
|
||||
if is_vector else f'["{prop}"]'
|
||||
|
||||
driver.driver.expression = expression
|
||||
|
||||
|
||||
def add_direct_driver(target, target_prop, source, source_prop):
|
||||
drivers = [
|
||||
{
|
||||
"source": source,
|
||||
"prop": source_prop
|
||||
}
|
||||
]
|
||||
expression = "var0"
|
||||
add_driver(target, target_prop, drivers, expression)
|
||||
|
||||
|
||||
def read_file_prop(content: str):
|
||||
props = {}
|
||||
for line in content.split("\n"):
|
||||
line = line.replace(" ", "")
|
||||
p = line.split("=")[0]
|
||||
if p != "":
|
||||
v = line.split("=")[-1]
|
||||
props[p] = v
|
||||
return props
|
||||
|
||||
|
||||
def write_file_prop(props: dict):
|
||||
lines = []
|
||||
for p, v in props.items():
|
||||
lines.append(f"{p} = {v}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def set_file_prop(prop, value):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
props[prop] = value
|
||||
bpy.data.texts["BioxelNodes"].clear()
|
||||
bpy.data.texts["BioxelNodes"].write(write_file_prop(props))
|
||||
|
||||
|
||||
def get_file_prop(prop):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
return props.get(prop)
|
||||
|
||||
|
||||
def is_incompatible():
|
||||
if get_file_prop("addon_version") is None:
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
return True
|
||||
else:
|
||||
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||
if addon_version[0] != VERSION[0]\
|
||||
or addon_version[1] != VERSION[1]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def local_lib_not_updated():
|
||||
use_local = False
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_group_lib = node_group.library
|
||||
if node_group_lib:
|
||||
abs_filepath = bpy.path.abspath(node_group_lib.filepath)
|
||||
_local_lib_file = Path(abs_filepath).resolve().as_posix()
|
||||
if _local_lib_file != NODE_LIB_FILEPATH.as_posix():
|
||||
use_local = True
|
||||
break
|
||||
|
||||
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||
not_update = addon_version != VERSION
|
||||
return use_local and not_update
|
||||
@@ -5,10 +5,13 @@ 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 ..nodes import custom_nodes
|
||||
from .node import get_nodes_by_type, move_node_to_node
|
||||
|
||||
from .layer import Layer, layer_to_obj, obj_to_layer
|
||||
from .common import (get_container_layer_objs,
|
||||
get_layer_prop_value,
|
||||
get_nodes_by_type,
|
||||
move_node_to_node)
|
||||
from .node import add_node_to_graph
|
||||
from ..utils import get_use_link
|
||||
|
||||
NODE_TYPE = {
|
||||
"label": "BioxelNodes_MaskByLabel",
|
||||
@@ -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,8 +77,38 @@ 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
|
||||
try:
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
except:
|
||||
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
for i, layer in enumerate(layers):
|
||||
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
|
||||
fetch_node = add_node_to_graph("FetchLayer",
|
||||
node_group,
|
||||
get_use_link())
|
||||
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,
|
||||
step_size: float,
|
||||
cache_dir: str):
|
||||
# Wrapper a Container
|
||||
|
||||
@@ -151,30 +139,33 @@ def container_to_obj(container: Container,
|
||||
container_obj.name = container.name
|
||||
container_obj.data.name = container.name
|
||||
container_obj.show_in_front = True
|
||||
|
||||
container_obj.lock_location[0] = True
|
||||
container_obj.lock_location[1] = True
|
||||
container_obj.lock_location[2] = True
|
||||
|
||||
container_obj.lock_rotation[0] = True
|
||||
container_obj.lock_rotation[1] = True
|
||||
container_obj.lock_rotation[2] = True
|
||||
|
||||
container_obj.lock_scale[0] = True
|
||||
container_obj.lock_scale[1] = True
|
||||
container_obj.lock_scale[2] = True
|
||||
|
||||
container_obj['bioxel_container'] = True
|
||||
container_obj["scene_scale"] = scene_scale
|
||||
container_obj["step_size"] = step_size
|
||||
|
||||
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="Output",
|
||||
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
|
||||
|
||||
@@ -7,35 +7,11 @@ import pyopenvdb as vdb
|
||||
from pathlib import Path
|
||||
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_use_link
|
||||
from .node import add_node_to_graph
|
||||
from .common import (get_layer_prop_value,
|
||||
move_node_between_nodes)
|
||||
|
||||
|
||||
def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
@@ -51,7 +27,11 @@ def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
grids, base_metadata = vdb.readAll(str(f))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
data_frame = np.ndarray(grid["data_shape"], np.float32)
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data_frame = np.ndarray(data_shape, np.float32)
|
||||
grid.copyToArray(data_frame)
|
||||
data_frames += (data_frame,)
|
||||
data = np.stack(data_frames)
|
||||
@@ -59,15 +39,24 @@ def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
grids, base_metadata = vdb.readAll(str(cache_filepath))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
data = np.ndarray(grid["data_shape"], np.float32)
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data = np.ndarray(data_shape, np.float32)
|
||||
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 +99,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 +108,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 +126,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 +144,35 @@ 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.render.space = 'WORLD'
|
||||
scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
step_size = container_obj.get("step_size") or 1
|
||||
layer_data.render.step_size = scene_scale * step_size
|
||||
layer_data.sequence_mode = 'REPEAT'
|
||||
layer_data.filepath = str(cache_filepaths[0])
|
||||
|
||||
layer_obj = bpy.context.active_object
|
||||
layer_obj.data.sequence_mode = 'REPEAT'
|
||||
|
||||
# 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
|
||||
if layer.frame_count > 1:
|
||||
layer_data.is_sequence = True
|
||||
layer_data.frame_duration = layer.frame_count
|
||||
else:
|
||||
layer_data.is_sequence = False
|
||||
|
||||
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
|
||||
layer_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes__Layer")
|
||||
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 = add_node_to_graph("_Layer", node_group, get_use_link())
|
||||
|
||||
layer_node.inputs['name'].default_value = layer.name
|
||||
layer_node.inputs['shape'].default_value = layer.shape
|
||||
@@ -207,19 +183,18 @@ def layer_to_obj(layer: Layer,
|
||||
affine_key = f"affine{i}{j}"
|
||||
layer_node.inputs[affine_key].default_value = layer.affine[j, i]
|
||||
|
||||
layer_node.inputs['id'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
layer_node.inputs['unique'].default_value = random.uniform(0, 1)
|
||||
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
|
||||
layer_node.inputs['frame_count'].default_value = layer.frame_count
|
||||
layer_node.inputs['channel_count'].default_value = layer.channel_count
|
||||
layer_node.inputs['offset'].default_value = max(0, -layer.min)
|
||||
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 +204,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
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
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]
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import bpy
|
||||
|
||||
from .common import get_file_prop, set_file_prop
|
||||
from ..exceptions import Incompatible, NoFound
|
||||
|
||||
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||
|
||||
|
||||
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)
|
||||
def get_node_group(node_type: str, use_link=True):
|
||||
# unannotate below for local debug in node lib file.
|
||||
# node_group = bpy.data.node_groups[node_type]
|
||||
# return node_group
|
||||
|
||||
node.location.x = sum(xs) / len(xs) + offset[0]
|
||||
node.location.y = sum(ys) / len(ys) + offset[1]
|
||||
if get_file_prop("addon_version") is None:
|
||||
set_file_prop("addon_version", VERSION)
|
||||
|
||||
local_lib_file = None
|
||||
addon_lib_file = NODE_LIB_FILEPATH.as_posix()
|
||||
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_group_lib = node_group.library
|
||||
if node_group_lib:
|
||||
abs_filepath = bpy.path.abspath(node_group_lib.filepath)
|
||||
_local_lib_file = Path(abs_filepath).resolve().as_posix()
|
||||
if _local_lib_file != addon_lib_file:
|
||||
local_lib_file = _local_lib_file
|
||||
break
|
||||
|
||||
# local lib first
|
||||
lib_file = local_lib_file or addon_lib_file
|
||||
bpy.ops.wm.append('EXEC_DEFAULT',
|
||||
directory=f"{lib_file}/NodeTree",
|
||||
filename=node_type,
|
||||
link=use_link,
|
||||
use_recursive=True,
|
||||
do_reuse_local_id=True)
|
||||
node_group = bpy.data.node_groups.get(node_type)
|
||||
|
||||
if node_group is None:
|
||||
raise NoFound('No custom node found')
|
||||
|
||||
return node_group
|
||||
|
||||
|
||||
def get_node_type(node):
|
||||
node_type = type(node).__name__
|
||||
if node_type == "GeometryNodeGroup":
|
||||
node_type = node.node_tree.name
|
||||
|
||||
return node_type
|
||||
def assign_node_group(node, node_type: str):
|
||||
node.node_tree = bpy.data.node_groups[node_type]
|
||||
node.width = 200.0
|
||||
node.name = node_type
|
||||
return node
|
||||
|
||||
|
||||
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 add_node_to_graph(node_name: str, node_group, use_link=True):
|
||||
node_type = f"BioxelNodes_{node_name}"
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in node_group.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
get_node_group(node_type, use_link)
|
||||
node = node_group.nodes.new("GeometryNodeGroup")
|
||||
assign_node_group(node, node_type)
|
||||
|
||||
node.show_options = False
|
||||
return node
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "bioxelnodes"
|
||||
version = "0.3.3"
|
||||
version = "1.0.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"
|
||||
|
||||
|
||||
224
bioxelnodes/constants.py
Normal file
224
bioxelnodes/constants.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from pathlib import Path
|
||||
|
||||
VERSION = (1, 0, 0)
|
||||
NODE_LIB_FILENAME = "BioxelNodes_v1.0.x"
|
||||
NODE_LIB_FILEPATH = Path(Path(__file__).parent,
|
||||
f"assets/Nodes/{NODE_LIB_FILENAME}.blend").resolve()
|
||||
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Component',
|
||||
'icon': 'OUTLINER_DATA_VOLUME',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Cutout by Threshold',
|
||||
'icon': 'EMPTY_SINGLE_ARROW',
|
||||
'name': 'CutoutByThreshold',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Range',
|
||||
'icon': 'IPO_CONSTANT',
|
||||
'name': 'CutoutByRange',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Color',
|
||||
'icon': 'COLOR',
|
||||
'name': 'CutoutByColor',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'To Surface',
|
||||
'icon': 'MESH_DATA',
|
||||
'name': 'ToSurface',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Join Component',
|
||||
'icon': 'CONSTRAINT_BONE',
|
||||
'name': 'JoinComponent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slice',
|
||||
'icon': 'TEXTURE',
|
||||
'name': 'Slice',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Properties',
|
||||
'icon': 'PROPERTIES',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Set Properties',
|
||||
'icon': 'PROPERTIES',
|
||||
'name': 'SetProperties',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Set Color',
|
||||
'icon': 'IPO_SINE',
|
||||
'name': 'SetColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Color',
|
||||
'icon': 'IPO_QUINT',
|
||||
'name': 'SetColorByColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 2',
|
||||
'icon': 'IPO_QUAD',
|
||||
'name': 'SetColorByRamp2',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 3',
|
||||
'icon': 'IPO_CUBIC',
|
||||
'name': 'SetColorByRamp3',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 4',
|
||||
'icon': 'IPO_QUART',
|
||||
'name': 'SetColorByRamp4',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 5',
|
||||
'icon': 'IPO_QUINT',
|
||||
'name': 'SetColorByRamp5',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Surface',
|
||||
'icon': 'MESH_DATA',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Membrane Shader',
|
||||
'icon': 'NODE_MATERIAL',
|
||||
'name': 'AssignMembraneShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Solid Shader',
|
||||
'icon': 'SHADING_SOLID',
|
||||
'name': 'AssignSolidShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slime Shader',
|
||||
'icon': 'OUTLINER_DATA_META',
|
||||
'name': 'AssignSlimeShader',
|
||||
'description': ''
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'name': 'Transform',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Transform Parent',
|
||||
'icon': 'ORIENTATION_PARENT',
|
||||
'name': 'TransformParent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'ReCenter',
|
||||
'icon': 'PROP_CON',
|
||||
'name': 'ReCenter',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Cutters',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Cut',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'name': 'Cut',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Primitive Cutter',
|
||||
'icon': 'MOD_LINEART',
|
||||
'name': 'PrimitiveCutter',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Object Cutter',
|
||||
'icon': 'MESH_PLANE',
|
||||
'name': 'ObjectCutter',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Utils',
|
||||
'icon': 'MODIFIER',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Pick Surface',
|
||||
'icon': 'OUTLINER_OB_MESH',
|
||||
'name': 'PickSurface',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Volume',
|
||||
'icon': 'OUTLINER_OB_VOLUME',
|
||||
'name': 'PickVolume',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Shape Wire',
|
||||
'icon': 'FILE_VOLUME',
|
||||
'name': 'PickShapeWire',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Bbox Wire',
|
||||
'icon': 'MESH_CUBE',
|
||||
'name': 'PickBboxWire',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Inflate',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'name': 'Inflate',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Smooth',
|
||||
'icon': 'MOD_SMOOTH',
|
||||
'name': 'Smooth',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Remove Small Island',
|
||||
'icon': 'FORCE_LENNARDJONES',
|
||||
'name': 'RemoveSmallIsland',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1 +0,0 @@
|
||||
from .menus import CustomNodes
|
||||
@@ -1,147 +0,0 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from .nodes import AddCustomNode
|
||||
|
||||
|
||||
class CustomNodes():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
nodes_file,
|
||||
class_prefix="CUSTOMNODES_MT_NODES",
|
||||
root_label='CustomNodes',
|
||||
root_icon='NONE'
|
||||
) -> None:
|
||||
if not Path(nodes_file).is_file():
|
||||
raise FileNotFoundError(str(Path(nodes_file).resolve()))
|
||||
|
||||
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(
|
||||
items=menu_items,
|
||||
label=root_label,
|
||||
menu_classes=menu_classes
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"{class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
|
||||
def add_node_menu(self, context):
|
||||
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(idname, icon=root_icon)
|
||||
|
||||
self.add_node_menu = add_node_menu
|
||||
|
||||
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 self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
for item in items:
|
||||
item_items = item.get('items') if item != 'separator' else None
|
||||
if item_items:
|
||||
menu_class = self._create_menu_class(
|
||||
menu_classes=menu_classes,
|
||||
items=item_items,
|
||||
label=item.get('label') or 'CustomNodes',
|
||||
icon=item.get('icon') or 0,
|
||||
idname_namespace=idname
|
||||
)
|
||||
item['menu_class'] = menu_class
|
||||
|
||||
# create menu class
|
||||
class Menu(bpy.types.Menu):
|
||||
bl_idname = idname
|
||||
bl_label = label
|
||||
nodes_file = self.nodes_file
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
for item in items:
|
||||
# print(item)
|
||||
if item == "separator":
|
||||
layout.separator()
|
||||
elif item.get('menu_class'):
|
||||
layout.menu(
|
||||
item.get('menu_class').bl_idname,
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
else:
|
||||
op = layout.operator(
|
||||
'customnodes.add_custom_node',
|
||||
text=item.get('label'),
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
op.nodes_file = nodes_file
|
||||
op.node_type = item['node_type']
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_description = item.get(
|
||||
'node_description') or "Add Custom Node."
|
||||
op.node_link = item.get('link') or True
|
||||
op.node_callback = item.get('node_callback') or ""
|
||||
|
||||
menu_classes.append(Menu)
|
||||
return Menu
|
||||
|
||||
def _find_item(self, found_items, menu_items, node_type: str):
|
||||
|
||||
for item in menu_items:
|
||||
if item == 'separator':
|
||||
continue
|
||||
|
||||
if item.get("node_type") == node_type:
|
||||
found_items.append(item)
|
||||
|
||||
item_items = item.get('items')
|
||||
if item_items:
|
||||
self._find_item(found_items, item_items, node_type)
|
||||
|
||||
def find_item(self, node_type: str):
|
||||
found_items = []
|
||||
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_group, node_type: str):
|
||||
item = self.find_item(node_type)
|
||||
op = AddCustomNode()
|
||||
op.nodes_file = self.nodes_file
|
||||
op.node_type = node_type
|
||||
if item:
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_link = item.get('link') or True
|
||||
op.node_callback = item.get('node_callback') or ""
|
||||
else:
|
||||
op.node_label = ""
|
||||
op.node_link = True
|
||||
op.node_callback = ""
|
||||
return op.add_node(node_group)
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.NODE_MT_add.append(self.add_node_menu)
|
||||
bpy.types.Scene.customnodes_node_dir = bpy.props.StringProperty(
|
||||
name="Node Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
)
|
||||
|
||||
def unregister(self):
|
||||
bpy.types.NODE_MT_add.remove(self.add_node_menu)
|
||||
try:
|
||||
for cls in reversed(self.menu_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
except RuntimeError:
|
||||
pass
|
||||
@@ -1,127 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class AddCustomNode():
|
||||
|
||||
nodes_file: bpy.props.StringProperty(
|
||||
name="nodes_file",
|
||||
subtype='FILE_PATH',
|
||||
default=""
|
||||
) # type: ignore
|
||||
|
||||
node_type: bpy.props.StringProperty(
|
||||
name='node_type',
|
||||
description='',
|
||||
default='',
|
||||
subtype='NONE',
|
||||
maxlen=0
|
||||
) # type: ignore
|
||||
|
||||
node_label: bpy.props.StringProperty(
|
||||
name='node_label',
|
||||
default=''
|
||||
) # type: ignore
|
||||
|
||||
node_description: bpy.props.StringProperty(
|
||||
name="node_description",
|
||||
description="",
|
||||
default="Add custom node group.",
|
||||
subtype="NONE"
|
||||
) # type: ignore
|
||||
|
||||
node_link: bpy.props.BoolProperty(
|
||||
name='node_link',
|
||||
default=True
|
||||
) # type: ignore
|
||||
|
||||
node_callback: bpy.props.StringProperty(
|
||||
name='node_callback',
|
||||
default=''
|
||||
) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
return properties.node_description
|
||||
|
||||
def assign_node_tree(self, node):
|
||||
node.node_tree = bpy.data.node_groups[self.node_type]
|
||||
|
||||
node.width = 200.0
|
||||
node.label = self.node_label or self.node_type
|
||||
node.name = self.node_type
|
||||
|
||||
if self.node_callback:
|
||||
exec(self.node_callback)
|
||||
|
||||
return node
|
||||
|
||||
def get_node_tree(self, node_type, node_link):
|
||||
# try to get node from current file if exists
|
||||
node_tree = bpy.data.node_groups.get(node_type)
|
||||
# if not exists, get it from asset file.
|
||||
if not node_tree:
|
||||
bpy.ops.wm.append(
|
||||
'EXEC_DEFAULT',
|
||||
directory=f"{self.nodes_file}/NodeTree",
|
||||
filename=node_type,
|
||||
link=node_link,
|
||||
use_recursive=True
|
||||
)
|
||||
|
||||
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
|
||||
for node in node_group.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
self.get_node_tree(self.node_type, self.node_link)
|
||||
node = node_group.nodes.new("GeometryNodeGroup")
|
||||
self.assign_node_tree(node)
|
||||
node.show_options = False
|
||||
|
||||
return node
|
||||
|
||||
|
||||
class CUSTOMNODES_OT_Add_Custom_Node(bpy.types.Operator, AddCustomNode):
|
||||
bl_idname = "customnodes.add_custom_node"
|
||||
bl_label = "Add Custom Node"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
self.get_node_tree(self.node_type, self.node_link)
|
||||
|
||||
# intended to be called upon button press in the node tree
|
||||
prev_context = bpy.context.area.type
|
||||
bpy.context.area.type = 'NODE_EDITOR'
|
||||
# actually invoke the operator to add a node to the current node tree
|
||||
# use_transform=True ensures it appears where the user's mouse is and is currently
|
||||
# being moved so the user can place it where they wish
|
||||
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
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -2,3 +2,21 @@ class CancelledByUser(Exception):
|
||||
def __init__(self):
|
||||
message = 'Cancelled by user'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class NoContent(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class NoFound(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class Incompatible(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
@@ -1,156 +1,160 @@
|
||||
import bpy
|
||||
|
||||
from .bioxelutils.container import get_container_objs_from_selection
|
||||
from .operators.layer import (CombineLabels, SignScalar,
|
||||
from .constants import MENU_ITEMS, NODE_LIB_FILENAME
|
||||
from .node_menu import NodeMenu
|
||||
|
||||
from .bioxelutils.common import (get_container_obj,
|
||||
get_container_layer_objs, get_layer_label,
|
||||
get_layer_prop_value)
|
||||
from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer,
|
||||
RemoveSelectedLayers, SaveSelectedLayersCache,
|
||||
ResampleLayer, SignScalar, CombineLabels,
|
||||
FillByLabel, FillByThreshold, FillByRange)
|
||||
from .operators.container import (SaveContainer, LoadContainer,
|
||||
from .operators.container import (AddLocator, AddSlicer, ExtractShapeWire,
|
||||
SaveContainerLayersCache, RemoveContainerMissingLayers,
|
||||
SaveContainer, LoadContainer,
|
||||
AddPieCutter, AddPlaneCutter,
|
||||
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
|
||||
PickBboxWire, PickMesh, PickVolume)
|
||||
from .operators.io import (ExportVolumetricData,
|
||||
ImportAsLabelLayer, ImportAsScalarLayer)
|
||||
from .operators.misc import (CleanAllCaches,
|
||||
ReLinkNodes, SaveCaches, SaveStagedData)
|
||||
ExtractBboxWire, ExtractSurface, ExtractVolume, ContainerProps)
|
||||
|
||||
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
|
||||
from .operators.misc import (CleanTemp,
|
||||
ReLinkNodeLib, RemoveAllMissingLayers, RenderSettingPreset, SaveAllLayersCache, SaveNodeLib, SliceViewer)
|
||||
|
||||
|
||||
def container_is_selected():
|
||||
container_objs = get_container_objs_from_selection()
|
||||
return len(container_objs) > 0
|
||||
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_label(layer_obj))
|
||||
op.layer_obj_name = layer_obj.name
|
||||
|
||||
|
||||
class PickFromContainerMenu(bpy.types.Menu):
|
||||
class ExtractFromContainerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_PICK"
|
||||
bl_label = "Pick from Container"
|
||||
bl_label = "Extract from Container"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(PickMesh.bl_idname)
|
||||
layout.operator(PickVolume.bl_idname)
|
||||
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)
|
||||
layout.operator(ExtractSurface.bl_idname)
|
||||
layout.operator(ExtractVolume.bl_idname)
|
||||
layout.operator(ExtractShapeWire.bl_idname)
|
||||
layout.operator(ExtractBboxWire.bl_idname)
|
||||
|
||||
|
||||
class AddCutterMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
||||
bl_label = "Add a Cutter to Container"
|
||||
bl_label = "Add a Cutter"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(AddPlaneCutter.bl_idname, text="Plane Cutter")
|
||||
layout.operator(AddCylinderCutter.bl_idname, text="Cylinder Cutter")
|
||||
layout.operator(AddCubeCutter.bl_idname, text="Cube Cutter")
|
||||
layout.operator(AddSphereCutter.bl_idname, text="Sphere Cutter")
|
||||
layout.operator(AddPieCutter.bl_idname, text="Pie Cutter")
|
||||
layout.operator(AddPlaneCutter.bl_idname,
|
||||
icon=AddPlaneCutter.bl_icon, text="Plane Cutter")
|
||||
layout.operator(AddCylinderCutter.bl_idname,
|
||||
icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter")
|
||||
layout.operator(AddCubeCutter.bl_idname,
|
||||
icon=AddCubeCutter.bl_icon, text="Cube Cutter")
|
||||
layout.operator(AddSphereCutter.bl_idname,
|
||||
icon=AddSphereCutter.bl_icon, text="Sphere Cutter")
|
||||
layout.operator(AddPieCutter.bl_idname,
|
||||
icon=AddPieCutter.bl_icon, text="Pie Cutter")
|
||||
|
||||
|
||||
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)"
|
||||
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):
|
||||
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.operator(SignScalar.bl_idname,
|
||||
icon=SignScalar.bl_icon)
|
||||
layout.operator(FillByThreshold.bl_idname,
|
||||
icon=FillByThreshold.bl_icon)
|
||||
layout.operator(FillByRange.bl_idname,
|
||||
icon=FillByRange.bl_icon)
|
||||
layout.operator(FillByLabel.bl_idname,
|
||||
icon=FillByLabel.bl_icon)
|
||||
layout.operator(CombineLabels.bl_idname,
|
||||
icon=CombineLabels.bl_icon)
|
||||
|
||||
|
||||
class ReLinkNodeLibMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_RELINK"
|
||||
bl_label = "Relink Node Library"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
versions = [{"label": "v0.3.x", "filename": "BioxelNodes_v0.3.x"},
|
||||
{"label": "v0.2.x", "filename": "BioxelNodes_v0.2.x"},
|
||||
{"label": "v0.1.x", "filename": "BioxelNodes_v0.1.x"}]
|
||||
|
||||
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||
text="Current Version")
|
||||
op.node_lib_filename = NODE_LIB_FILENAME
|
||||
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)
|
||||
for version in versions:
|
||||
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||
text=version["label"])
|
||||
op.node_lib_filename = version["filename"]
|
||||
|
||||
|
||||
def TOPBAR_FILE_IMPORT(self, context):
|
||||
layout = self.layout
|
||||
is_selected = container_is_selected()
|
||||
class RenderSettingMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_RENDER"
|
||||
bl_label = "Render Setting Presets"
|
||||
|
||||
layout.separator()
|
||||
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
|
||||
if is_selected else "Volumetric Data as Bioxel (Init)")
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for k, v in RenderSettingPreset.PRESETS.items():
|
||||
op = layout.operator(RenderSettingPreset.bl_idname,
|
||||
text=v)
|
||||
op.preset = k
|
||||
|
||||
|
||||
def TOPBAR_FILE_EXPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumetricData.bl_idname,
|
||||
text="Bioxel Layer (.vdb)")
|
||||
class DangerZoneMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_DANGER"
|
||||
bl_label = "Danger Zone"
|
||||
|
||||
|
||||
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")
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(CleanTemp.bl_idname)
|
||||
|
||||
|
||||
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
@@ -159,21 +163,24 @@ 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.operator(SaveAllLayersCache.bl_idname)
|
||||
layout.operator(RemoveAllMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveStagedData.bl_idname)
|
||||
layout.operator(ReLinkNodes.bl_idname)
|
||||
layout.operator(SaveNodeLib.bl_idname)
|
||||
layout.menu(ReLinkNodeLibMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(CleanAllCaches.bl_idname)
|
||||
layout.menu(DangerZoneMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(RenderSettingMenu.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR(self, context):
|
||||
@@ -181,17 +188,194 @@ def TOPBAR(self, context):
|
||||
layout.menu(BioxelNodesTopbarMenu.bl_idname)
|
||||
|
||||
|
||||
class NodeHeadMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_HEAD"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
layout = self.layout
|
||||
layout.menu(AddLayerMenu.bl_idname,
|
||||
icon=AddLayerMenu.bl_icon)
|
||||
layout.operator(SaveContainer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ContainerProps.bl_idname)
|
||||
layout.menu(ExtractFromContainerMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveContainerLayersCache.bl_idname,
|
||||
icon=SaveContainerLayersCache.bl_icon)
|
||||
layout.operator(RemoveContainerMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(AddLocator.bl_idname,
|
||||
icon=AddLocator.bl_icon)
|
||||
layout.operator(AddSlicer.bl_idname,
|
||||
icon=AddSlicer.bl_icon)
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(FetchLayerMenu.bl_idname)
|
||||
|
||||
|
||||
class NodeContextMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_CONTEXT"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SaveSelectedLayersCache.bl_idname,
|
||||
icon=SaveSelectedLayersCache.bl_icon)
|
||||
layout.operator(RemoveSelectedLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(RenameLayer.bl_idname,
|
||||
icon=RenameLayer.bl_icon)
|
||||
layout.operator(RetimeLayer.bl_idname)
|
||||
layout.operator(RelocateLayer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ResampleLayer.bl_idname,
|
||||
icon=ResampleLayer.bl_icon)
|
||||
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 NODE_CONTEXT(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(NodeContextMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_HEAD(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(NodeHeadMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_PROP(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.clear()
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
layer_item = layer_list.add()
|
||||
layer_item.label = get_layer_label(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",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
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(SaveContainerLayersCache.bl_idname,
|
||||
# icon=SaveContainerLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveContainerMissingLayers.bl_idname,
|
||||
# icon=RemoveContainerMissingLayers.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SaveSelectedLayersCache.bl_idname,
|
||||
# icon=SaveSelectedLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveSelectedLayers.bl_idname,
|
||||
# icon=RemoveSelectedLayers.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(RenameLayer.bl_idname,
|
||||
# icon=RenameLayer.bl_icon, text="")
|
||||
# sidebar.operator(ResampleLayer.bl_idname,
|
||||
# icon=ResampleLayer.bl_icon, text="")
|
||||
# sidebar.operator(RetimeLayer.bl_idname,
|
||||
# icon=RetimeLayer.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SignScalar.bl_idname,
|
||||
# icon=SignScalar.bl_icon, text="")
|
||||
# sidebar.operator(FillByThreshold.bl_idname,
|
||||
# icon=FillByThreshold.bl_icon, text="")
|
||||
# sidebar.operator(FillByRange.bl_idname,
|
||||
# icon=FillByRange.bl_icon, text="")
|
||||
# sidebar.operator(FillByLabel.bl_idname,
|
||||
# icon=FillByLabel.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# layout.separator()
|
||||
|
||||
|
||||
def VIEW3D_TOPBAR(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SliceViewer.bl_idname,
|
||||
icon=SliceViewer.bl_icon, text="")
|
||||
|
||||
|
||||
node_menu = NodeMenu(
|
||||
menu_items=MENU_ITEMS
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
node_menu.register()
|
||||
bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.append(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT)
|
||||
|
||||
|
||||
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.VIEW3D_HT_header.remove(VIEW3D_TOPBAR)
|
||||
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.remove(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.remove(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT)
|
||||
node_menu.unregister()
|
||||
|
||||
87
bioxelnodes/node_menu.py
Normal file
87
bioxelnodes/node_menu.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import bpy
|
||||
from .operators.node import AddNode
|
||||
|
||||
|
||||
class NodeMenu():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
) -> None:
|
||||
|
||||
self.menu_items = menu_items
|
||||
self.class_prefix = f"BIOXELNODES_MT"
|
||||
root_label = "Bioxel Nodes"
|
||||
menu_classes = []
|
||||
self._create_menu_class(
|
||||
items=menu_items,
|
||||
label=root_label,
|
||||
menu_classes=menu_classes
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"{self.class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
icon = "FILE_VOLUME"
|
||||
|
||||
def drew_menu(self, context):
|
||||
if (bpy.context.area.spaces[0].tree_type == 'GeometryNodeTree'):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(idname,
|
||||
icon=icon)
|
||||
self.drew_menu = drew_menu
|
||||
|
||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
||||
idname_namespace = idname_namespace or self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
for item in items:
|
||||
item_items = item.get('items') if item != 'separator' else None
|
||||
if item_items:
|
||||
menu_class = self._create_menu_class(
|
||||
menu_classes=menu_classes,
|
||||
items=item_items,
|
||||
label=item.get('label') or 'CustomNodes',
|
||||
icon=item.get('icon') or 0,
|
||||
idname_namespace=idname
|
||||
)
|
||||
item['menu_class'] = menu_class
|
||||
|
||||
# create menu class
|
||||
class Menu(bpy.types.Menu):
|
||||
bl_idname = idname
|
||||
bl_label = label
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for item in items:
|
||||
if item == "separator":
|
||||
layout.separator()
|
||||
elif item.get('menu_class'):
|
||||
layout.menu(
|
||||
item.get('menu_class').bl_idname,
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
else:
|
||||
op = layout.operator(
|
||||
AddNode.bl_idname,
|
||||
text=item.get('label'),
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
op.node_name = item['name']
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_description = item.get(
|
||||
'node_description') or ""
|
||||
|
||||
menu_classes.append(Menu)
|
||||
return Menu
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.NODE_MT_add.append(self.drew_menu)
|
||||
|
||||
def unregister(self):
|
||||
bpy.types.NODE_MT_add.remove(self.drew_menu)
|
||||
for cls in reversed(self.menu_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -1,228 +0,0 @@
|
||||
|
||||
from pathlib import Path
|
||||
from .customnodes import CustomNodes
|
||||
|
||||
NODE_FILE = "BioxelNodes_4.2"
|
||||
|
||||
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': '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',
|
||||
'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",
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
custom_nodes.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
custom_nodes.unregister()
|
||||
@@ -1,15 +1,17 @@
|
||||
import bpy
|
||||
|
||||
|
||||
import bmesh
|
||||
|
||||
from ..nodes import custom_nodes
|
||||
from ..utils import get_cache_dir, get_use_link, select_object
|
||||
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.node import add_node_to_graph
|
||||
from ..bioxelutils.common import (get_container_layer_objs, get_container_obj,
|
||||
get_nodes_by_type, is_missing_layer,
|
||||
move_node_between_nodes,
|
||||
move_node_to_node,
|
||||
get_all_layer_objs)
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -65,11 +62,12 @@ class LoadContainer(bpy.types.Operator):
|
||||
|
||||
container_obj = container_to_obj(container,
|
||||
scene_scale=0.01,
|
||||
cache_dir=get_cache_dir(context))
|
||||
cache_dir=get_cache_dir())
|
||||
select_object(container_obj)
|
||||
|
||||
if is_first_import:
|
||||
change_render_setting(context)
|
||||
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||
preset="preview_c")
|
||||
|
||||
self.report({"INFO"}, f"Successfully load {load_path}")
|
||||
return {'FINISHED'}
|
||||
@@ -79,39 +77,33 @@ class LoadContainer(bpy.types.Operator):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class PickObject():
|
||||
class ExtractObject():
|
||||
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
|
||||
|
||||
obj.name = f"{self.object_type}_{container_obj.name}"
|
||||
obj.name = f"{container_obj.name}_{self.object_type}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
pick_mesh_node = custom_nodes.add_node(node_group,
|
||||
f"BioxelNodes_Pick{self.object_type}")
|
||||
fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
pick_mesh_node.inputs[0].default_value = container_obj
|
||||
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
||||
fetch_mesh_node.inputs[0].default_value = container_obj
|
||||
node_group.links.new(fetch_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
select_object(obj)
|
||||
|
||||
@@ -120,71 +112,78 @@ class PickObject():
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PickMesh(bpy.types.Operator, PickObject):
|
||||
bl_idname = "bioxelnodes.pick_mesh"
|
||||
bl_label = "Pick Mesh"
|
||||
bl_description = "Pick Container Mesh"
|
||||
object_type = "Mesh"
|
||||
class ExtractSurface(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.fetch_mesh"
|
||||
bl_label = "Extract Surface"
|
||||
bl_description = "Extract Surface"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
object_type = "Surface"
|
||||
|
||||
|
||||
class PickVolume(bpy.types.Operator, PickObject):
|
||||
class ExtractVolume(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_volume"
|
||||
bl_label = "Pick Volume"
|
||||
bl_description = "Pick Container Volume"
|
||||
bl_label = "Extract Volume"
|
||||
bl_description = "Extract Volume"
|
||||
bl_icon = "OUTLINER_OB_VOLUME"
|
||||
object_type = "Volume"
|
||||
|
||||
|
||||
class PickBboxWire(bpy.types.Operator, PickObject):
|
||||
class ExtractShapeWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_shape_wire"
|
||||
bl_label = "Extract Shape Wire"
|
||||
bl_description = "Extract Shape Wire"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
object_type = "ShapeWire"
|
||||
|
||||
|
||||
class ExtractBboxWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
||||
bl_label = "Pick Bbox Wire"
|
||||
bl_description = "Pick Container Bbox Wire"
|
||||
bl_label = "Extract Bbox Wire"
|
||||
bl_description = "Extract 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":
|
||||
bpy.ops.mesh.primitive_plane_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
pie_obj = bpy.context.active_object
|
||||
orig_data = pie_obj.data
|
||||
|
||||
# Create mesh
|
||||
pie_mesh = bpy.data.meshes.new('Pie')
|
||||
pie_obj.data = pie_mesh
|
||||
bpy.data.meshes.remove(orig_data)
|
||||
|
||||
# Create object
|
||||
pie = bpy.data.objects.new('Pie', pie_mesh)
|
||||
# # Create object
|
||||
# pie = bpy.data.objects.new('Pie', pie_mesh)
|
||||
|
||||
# # Link object to scene
|
||||
# bpy.context.scene.collection.objects.link(pie)
|
||||
|
||||
# Link object to scene
|
||||
bpy.context.scene.collection.objects.link(pie)
|
||||
# Get a BMesh representation
|
||||
bm = bmesh.new() # create an empty BMesh
|
||||
bm.from_mesh(pie_mesh) # fill it in from a Mesh
|
||||
@@ -208,9 +207,22 @@ class AddCutter():
|
||||
|
||||
# Finish up, write the bmesh back to the mesh
|
||||
bm.to_mesh(pie_mesh)
|
||||
bpy.context.view_layer.objects.active = pie
|
||||
# bpy.context.view_layer.objects.active = pie
|
||||
|
||||
cutter_obj = bpy.context.active_object
|
||||
|
||||
# vcos = [container_obj.matrix_world @
|
||||
# v.co for v in container_obj.data.vertices]
|
||||
|
||||
# def find_center(l): return (max(l) + min(l)) / 2
|
||||
|
||||
# x, y, z = [[v[i] for v in vcos] for i in range(3)]
|
||||
# center = [find_center(axis) for axis in [x, y, z]]
|
||||
|
||||
# cutter_obj.location = center
|
||||
name = self.cutter_type.capitalize()
|
||||
cutter_obj.name = name
|
||||
cutter_obj.data.name = name
|
||||
cutter_obj.visible_camera = False
|
||||
cutter_obj.visible_diffuse = False
|
||||
cutter_obj.visible_glossy = False
|
||||
@@ -219,17 +231,28 @@ class AddCutter():
|
||||
cutter_obj.visible_shadow = False
|
||||
cutter_obj.hide_render = True
|
||||
cutter_obj.display_type = 'WIRE'
|
||||
cutter_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
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 = add_node_to_graph("ObjectCutter",
|
||||
node_group,
|
||||
get_use_link())
|
||||
cutter_node.inputs[0].default_value = self.cutter_type.capitalize()
|
||||
cutter_node.inputs[1].default_value = cutter_obj
|
||||
|
||||
cut_node = add_node_to_graph("Cut",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
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,10 +270,13 @@ 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.add_node('EXEC_DEFAULT',
|
||||
node_name="ObjectCutter",
|
||||
node_label=name)
|
||||
node = bpy.context.active_node
|
||||
node.inputs[0].default_value = name
|
||||
node.inputs[1].default_value = cutter_obj
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -258,33 +284,242 @@ class AddCutter():
|
||||
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_description = "Add a plane cutter to current container"
|
||||
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_description = "Add a cylinder cutter to current container"
|
||||
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_description = "Add a cube cutter to current container"
|
||||
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_description = "Add a sphere cutter to current container"
|
||||
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_description = "Add a pie cutter to current container"
|
||||
bl_icon = "MESH_CONE"
|
||||
cutter_type = "pie"
|
||||
|
||||
|
||||
class AddSlicer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_slicer"
|
||||
bl_label = "Add a Slicer"
|
||||
bl_description = "Add a slicer to current container"
|
||||
bl_icon = "TEXTURE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
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'}
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=2,
|
||||
enter_editmode=False,
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
slicer_obj = bpy.context.active_object
|
||||
slicer_obj.name = "Slicer"
|
||||
slicer_obj.data.name = "Slicer"
|
||||
|
||||
slicer_obj.visible_camera = False
|
||||
slicer_obj.visible_diffuse = False
|
||||
slicer_obj.visible_glossy = False
|
||||
slicer_obj.visible_transmission = False
|
||||
slicer_obj.visible_volume_scatter = False
|
||||
slicer_obj.visible_shadow = False
|
||||
slicer_obj.hide_render = True
|
||||
slicer_obj.display_type = 'WIRE'
|
||||
slicer_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
slice_node = add_node_to_graph("Slice",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
slice_node.inputs[1].default_value = slicer_obj
|
||||
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(slice_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],
|
||||
slice_node.inputs[0])
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(slice_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddLocator(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_locator"
|
||||
bl_label = "Add a Locator"
|
||||
bl_description = "Add a locator to current container"
|
||||
bl_icon = "EMPTY_AXIS"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
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'}
|
||||
|
||||
bpy.ops.object.empty_add(type='ARROWS',
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
locator_obj = bpy.context.active_object
|
||||
locator_obj.name = "Locator"
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
parent_node = add_node_to_graph("TransformParent",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
parent_node.inputs[1].default_value = locator_obj
|
||||
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(parent_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],
|
||||
parent_node.inputs[0])
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(parent_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ContainerProps(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.container_props"
|
||||
bl_label = "Change Container Properties"
|
||||
bl_description = "Change current ontainer properties"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
scene_scale: bpy.props.FloatProperty(name="Scene Scale",
|
||||
soft_min=0.0001, soft_max=10.0,
|
||||
min=1e-6, max=1e6,
|
||||
default=0.01) # type: ignore
|
||||
|
||||
step_size: bpy.props.FloatProperty(name="Step Size",
|
||||
soft_min=0.1, soft_max=100.0,
|
||||
min=0.1, max=1e2,
|
||||
default=1) # 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'}
|
||||
|
||||
container_obj.scale[0] = self.scene_scale
|
||||
container_obj.scale[1] = self.scene_scale
|
||||
container_obj.scale[2] = self.scene_scale
|
||||
container_obj["scene_scale"] = self.scene_scale
|
||||
container_obj["step_size"] = self.step_size
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
layer_obj.data.render.space = 'WORLD'
|
||||
layer_obj.data.render.step_size = self.scene_scale * self.step_size
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
return self.execute(context)
|
||||
else:
|
||||
self.scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
self.step_size = container_obj.get("step_size") or 1
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def get_container_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
container_obj = context.object
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
return [obj for obj in layer_objs if layer_filter(obj, context)]
|
||||
|
||||
|
||||
class SaveContainerLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_container_layers_cache"
|
||||
bl_label = "Save Container Layers' Cache"
|
||||
bl_description = "Save all current container layers' cache to directory."
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all container layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_not_missing)
|
||||
|
||||
|
||||
class RemoveContainerMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_container_missing_layers"
|
||||
bl_label = "Remove Container Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all container missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj, context):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_missing)
|
||||
|
||||
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'}
|
||||
|
||||
@@ -5,20 +5,20 @@ 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.common import (get_all_layer_objs, get_container_obj,
|
||||
get_layer_obj, is_incompatible)
|
||||
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,
|
||||
progress_update, progress_bar, select_object)
|
||||
|
||||
from ..utils import (get_cache_dir,
|
||||
progress_update, progress_bar,
|
||||
select_object)
|
||||
|
||||
# 3rd-party
|
||||
import SimpleITK as sitk
|
||||
@@ -57,7 +57,7 @@ class ImportVolumetricData():
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
|
||||
read_as = "scalar"
|
||||
read_as = "SCALAR"
|
||||
|
||||
def execute(self, context):
|
||||
data_path = Path(self.filepath).resolve()
|
||||
@@ -78,18 +78,28 @@ 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"
|
||||
read_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"
|
||||
read_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):
|
||||
@@ -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):
|
||||
@@ -123,9 +142,10 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
meta = None
|
||||
label_count = 0
|
||||
dtype = None
|
||||
thread = None
|
||||
_timer = None
|
||||
container_obj_name = ""
|
||||
|
||||
progress: bpy.props.FloatProperty(name="Progress",
|
||||
options={"SKIP_SAVE"},
|
||||
@@ -142,9 +162,10 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", "")]) # type: ignore
|
||||
default="SCALAR",
|
||||
items=[("SCALAR", "Scalar", ""),
|
||||
("LABEL", "Labels", ""),
|
||||
("COLOR", "Color", "")]) # type: ignore
|
||||
|
||||
series_id: bpy.props.EnumProperty(name="Select Series",
|
||||
items=get_series_ids) # type: ignore
|
||||
@@ -153,6 +174,15 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
type=BIOXELNODES_Series) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if is_incompatible():
|
||||
self.report({"ERROR"},
|
||||
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version")
|
||||
return {'CANCELLED'}
|
||||
|
||||
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:
|
||||
@@ -182,6 +212,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return
|
||||
|
||||
self.meta = meta
|
||||
self.label_count = int(np.max(data))
|
||||
self.dtype = data.dtype
|
||||
|
||||
# Init cancel flag
|
||||
self.is_cancelled = False
|
||||
@@ -207,7 +239,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 +259,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:
|
||||
@@ -246,25 +279,57 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
for key, value in self.meta.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
if self.read_as == "LABEL":
|
||||
if self.label_count > 100 or self.dtype.kind not in ["i", "u"]:
|
||||
self.report({"ERROR"}, "Invaild label data.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
orig_shape = self.meta['xyz_shape']
|
||||
orig_spacing = self.meta['spacing']
|
||||
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)
|
||||
min_log10 = math.floor(math.log10(min(*orig_spacing)))
|
||||
max_log10 = math.floor(math.log10(max(*orig_spacing)))
|
||||
# min_space = min(*orig_spacing)
|
||||
# max_space = max(*orig_spacing)
|
||||
|
||||
if orig_spacing[2] == 1 and min_log10 < -1:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-2),
|
||||
orig_spacing[1] * math.pow(10, -min_log10-2),
|
||||
1)
|
||||
elif min_log10 > 0:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-1),
|
||||
orig_spacing[1] * math.pow(10, -min_log10-1),
|
||||
orig_spacing[2] * math.pow(10, -min_log10-1))
|
||||
elif max_log10 < 0:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -max_log10-1),
|
||||
orig_spacing[1] * math.pow(10, -max_log10-1),
|
||||
orig_spacing[2] * math.pow(10, -max_log10-1))
|
||||
|
||||
bioxel_size = max(min(*orig_spacing), 1.0)
|
||||
|
||||
layer_shape = get_layer_shape(bioxel_size,
|
||||
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)
|
||||
bioxel_size,
|
||||
0.01)
|
||||
min_log10 = math.floor(math.log10(min(*layer_size)))
|
||||
max_log10 = math.floor(math.log10(max(*layer_size)))
|
||||
|
||||
if self.container_obj_name:
|
||||
container_obj = bpy.data.objects.get(self.container_obj_name)
|
||||
if min_log10 > 0:
|
||||
scene_scale = math.pow(10, -min_log10-2)
|
||||
elif max_log10 < 0:
|
||||
scene_scale = math.pow(10, -max_log10-2)
|
||||
else:
|
||||
scene_scale = 0.01
|
||||
|
||||
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,8 +343,9 @@ 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,
|
||||
label_count=self.label_count,
|
||||
scene_scale=scene_scale
|
||||
)
|
||||
|
||||
@@ -287,18 +353,22 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not self.filepath:
|
||||
# why not report in execute?
|
||||
# If this operator is executing, a new execute will when pre-one done.
|
||||
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 +437,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,
|
||||
@@ -386,17 +455,17 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
layout.prop(self, "series_id")
|
||||
|
||||
|
||||
def get_sequence_sources(self, context):
|
||||
items = [("-1", "None (1 frame)", "")]
|
||||
def get_frame_sources(self, context):
|
||||
items = [("-1", "First Frame (1 frame)", "")]
|
||||
orig_shape = tuple(self.orig_shape)
|
||||
if self.frame_count > 1:
|
||||
items.append(("0", f"Frame ({self.frame_count} frames)", ""))
|
||||
items.append(("0", f"Frames ({self.frame_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count > 1:
|
||||
items.append(("4", f"Channel ({self.channel_count} frames)", ""))
|
||||
items.append(("4", f"Channels ({self.channel_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count == 1:
|
||||
items.append(("1", f"X ({orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y ({orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z ({orig_shape[2]} frames)", ""))
|
||||
items.append(("1", f"X-axis ({orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y-axis ({orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z-axis ({orig_shape[2]} frames)", ""))
|
||||
|
||||
return items
|
||||
|
||||
@@ -426,10 +495,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
channel_count: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
label_count: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", "")]) # type: ignore
|
||||
default="SCALAR",
|
||||
items=[("SCALAR", "Scalar", ""),
|
||||
("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,
|
||||
@@ -447,11 +519,14 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
min=1e-6, max=1e6,
|
||||
default=0.01) # type: ignore
|
||||
|
||||
split_channels: bpy.props.BoolProperty(name="Split Channels",
|
||||
default=False) # type: ignore
|
||||
remap: bpy.props.BoolProperty(name="Remap to 0~1",
|
||||
default=False) # type: ignore
|
||||
|
||||
sequence_source: bpy.props.EnumProperty(name="Time Sequence From",
|
||||
items=get_sequence_sources) # type: ignore
|
||||
split_channel: bpy.props.BoolProperty(name="Split Channels",
|
||||
default=False) # type: ignore
|
||||
|
||||
frame_source: bpy.props.EnumProperty(name="Frame From",
|
||||
items=get_frame_sources) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
def import_volumetric_data_func(self, context, cancel):
|
||||
@@ -481,26 +556,26 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size)
|
||||
affine = np.dot(meta['affine'], mat_scale)
|
||||
kind = self.read_as
|
||||
kind = self.read_as.lower()
|
||||
|
||||
if cancel():
|
||||
return
|
||||
|
||||
# change shape as sequence or not
|
||||
if self.sequence_source == "-1":
|
||||
if self.frame_source == "-1":
|
||||
data = data[0:1, :, :, :, :]
|
||||
elif self.sequence_source == "0":
|
||||
elif self.frame_source == "0":
|
||||
# frame as frame
|
||||
pass
|
||||
elif self.sequence_source == "1":
|
||||
elif self.frame_source == "1":
|
||||
# X as frame
|
||||
data = data.transpose(1, 0, 2, 3, 4)
|
||||
shape = (1, shape[1], shape[2])
|
||||
elif self.sequence_source == "2":
|
||||
elif self.frame_source == "2":
|
||||
# Y as frame
|
||||
data = data.transpose(2, 1, 0, 3, 4)
|
||||
shape = (shape[0], 1, shape[2])
|
||||
elif self.sequence_source == "3":
|
||||
elif self.frame_source == "3":
|
||||
# Z as frame
|
||||
data = data.transpose(3, 1, 2, 0, 4)
|
||||
shape = (shape[0], shape[1], 1)
|
||||
@@ -516,6 +591,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
sub_progress = progress + frame * sub_progress_step
|
||||
progress_update(context, sub_progress,
|
||||
f"Processing {layer_name} Frame {frame+1}...")
|
||||
print(f"Processing {layer_name} Frame {frame+1}...")
|
||||
return progress_callback
|
||||
|
||||
layers = []
|
||||
@@ -540,12 +616,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,10 +630,82 @@ 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']:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
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)
|
||||
|
||||
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"
|
||||
|
||||
if self.split_channels:
|
||||
if self.remap:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
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)
|
||||
|
||||
if self.split_channel:
|
||||
progress_step = 0.7/self.channel_count
|
||||
|
||||
for i in range(self.channel_count):
|
||||
@@ -573,12 +722,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 +748,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 +785,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 +798,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.")
|
||||
@@ -670,20 +822,25 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
container_obj = add_layers(self.layers,
|
||||
container_obj=container_obj,
|
||||
cache_dir=get_cache_dir(context))
|
||||
cache_dir=get_cache_dir())
|
||||
else:
|
||||
container = Container(name=self.container_name,
|
||||
name = self.container_name or "Container"
|
||||
container = Container(name=name,
|
||||
layers=self.layers)
|
||||
|
||||
step_size = container.layers[0].bioxel_size[0]*5
|
||||
container_obj = container_to_obj(container,
|
||||
scene_scale=self.scene_scale,
|
||||
cache_dir=get_cache_dir(context))
|
||||
step_size=step_size,
|
||||
cache_dir=get_cache_dir())
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
# Change render setting for better result
|
||||
if is_first_import:
|
||||
change_render_setting(context)
|
||||
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||
preset="preview_c")
|
||||
# bpy.ops.bioxelnodes.slice_viewer('EXEC_DEFAULT')
|
||||
|
||||
self.report({"INFO"}, "Successfully Imported")
|
||||
return {'FINISHED'}
|
||||
@@ -706,54 +863,69 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
# change shape as sequence or not
|
||||
channel_count = self.channel_count
|
||||
if self.sequence_source == "-1":
|
||||
frame_count = self.frame_count
|
||||
if self.frame_source == "-1":
|
||||
frame_count = 1
|
||||
pass
|
||||
elif self.sequence_source == "0":
|
||||
elif self.frame_source == "0":
|
||||
# frame as frame
|
||||
pass
|
||||
elif self.sequence_source == "1":
|
||||
elif self.frame_source == "1":
|
||||
frame_count = orig_shape[0]
|
||||
layer_shape = (1, layer_shape[1], layer_shape[2])
|
||||
elif self.sequence_source == "2":
|
||||
elif self.frame_source == "2":
|
||||
frame_count = orig_shape[1]
|
||||
layer_shape = (layer_shape[0], 1, layer_shape[2])
|
||||
elif self.sequence_source == "3":
|
||||
elif self.frame_source == "3":
|
||||
frame_count = orig_shape[2]
|
||||
layer_shape = (layer_shape[0], layer_shape[1], 1)
|
||||
else:
|
||||
channel_count = 1
|
||||
|
||||
import_channel = channel_count if self.split_channels or channel_count == 1 else "combined"
|
||||
if self.read_as == "SCALAR":
|
||||
layer_count = channel_count if self.split_channel else 1
|
||||
channel_count = 1
|
||||
elif self.read_as == "LABEL":
|
||||
layer_count = self.label_count
|
||||
channel_count = 1
|
||||
else:
|
||||
layer_count = 1
|
||||
channel_count = 3
|
||||
|
||||
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
||||
layer_shape_text = f"Shape from {str(orig_shape)} to {str(layer_shape)}"
|
||||
orig_shape_text = f"[{self.frame_count}, ({orig_shape[0]},{orig_shape[1]},{orig_shape[2]}), {self.channel_count}]"
|
||||
layer_shape_text = f"{layer_count} x [{frame_count}, ({layer_shape[0]},{layer_shape[1]},{layer_shape[2]}), {channel_count}]"
|
||||
|
||||
if bioxel_count > 100000000:
|
||||
layer_shape_text += "**TOO LARGE!**"
|
||||
|
||||
layer_size = get_layer_size(layer_shape,
|
||||
self.bioxel_size,
|
||||
self.scene_scale)
|
||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||
|
||||
layout = self.layout
|
||||
if self.container_obj_name == "":
|
||||
layout.prop(self, "container_name")
|
||||
layout.prop(self, "layer_name")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "container_name")
|
||||
panel.prop(self, "scene_scale")
|
||||
panel.label(text=layer_size_text)
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "layer_name")
|
||||
panel.prop(self, "bioxel_size")
|
||||
row = panel.row()
|
||||
row.prop(self, "orig_spacing")
|
||||
panel.label(text=layer_shape_text)
|
||||
panel.prop(self, "frame_source")
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "sequence_source")
|
||||
if self.read_as == "SCALAR":
|
||||
panel.prop(self, "split_channel",
|
||||
text=f"Split Channel as Multi Layer")
|
||||
|
||||
if self.read_as == "scalar":
|
||||
panel = layout.box()
|
||||
panel.prop(self, "split_channels",
|
||||
text=f"Split Channels (Get {channel_count} channels, import {import_channel} channels)")
|
||||
|
||||
if self.container_obj_name == "":
|
||||
layer_size = get_layer_size(layer_shape,
|
||||
self.bioxel_size,
|
||||
self.scene_scale)
|
||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||
panel = layout.box()
|
||||
panel.prop(self, "scene_scale")
|
||||
panel.label(text=layer_size_text)
|
||||
panel.label(
|
||||
text="Dimension Order: [Frame, (X-axis,Y-axis,Z-axis), Channel]")
|
||||
panel.label(
|
||||
text=f"Shape from {orig_shape_text} to {layer_shape_text}")
|
||||
|
||||
|
||||
class ExportVolumetricData(bpy.types.Operator):
|
||||
|
||||
@@ -3,21 +3,14 @@ import bpy
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
from ..exceptions import NoContent
|
||||
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 ..bioxelutils.common import (get_container_obj, get_layer_kind, get_layer_label, get_layer_name,
|
||||
get_layer_prop_value,
|
||||
get_container_layer_objs,
|
||||
get_node_type, is_missing_layer, set_layer_prop_value)
|
||||
from ..bioxelutils.layer import layer_to_obj, obj_to_layer
|
||||
from ..utils import get_cache_dir, copy_to_dir
|
||||
|
||||
|
||||
def get_label_layer_selection(self, context):
|
||||
@@ -25,86 +18,327 @@ def get_label_layer_selection(self, context):
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
if get_layer_prop_value(layer_obj, "kind") == "label":
|
||||
kind = get_layer_prop_value(layer_obj, "kind")
|
||||
name = get_layer_prop_value(layer_obj, "name")
|
||||
if kind == "label":
|
||||
items.append((layer_obj.name,
|
||||
layer_obj.name,
|
||||
name,
|
||||
""))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
class LayerOperator():
|
||||
class FetchLayer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fetch_layer"
|
||||
bl_label = "Fetch Layer"
|
||||
bl_description = "Fetch layer from current container"
|
||||
bl_icon = "NODE"
|
||||
bl_options = {'UNDO'}
|
||||
base_obj: bpy.types.Object = None
|
||||
|
||||
def add_layer(self, context, layer):
|
||||
preferences = get_preferences(context)
|
||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
||||
container_obj = self.base_obj.parent
|
||||
layer_obj_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN"}) # type: ignore
|
||||
|
||||
add_layers([layer],
|
||||
container_obj,
|
||||
cache_dir)
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.base_obj = get_layer_obj(bpy.context.active_object)
|
||||
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
|
||||
def description(cls, context, properties):
|
||||
layer_obj_name = properties.layer_obj_name
|
||||
layer_obj = bpy.data.objects.get(layer_obj_name)
|
||||
return "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
|
||||
for prop in ["kind",
|
||||
"bioxel_size",
|
||||
"shape",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
|
||||
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 SignScalar(bpy.types.Operator, ScalarOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Scalar"
|
||||
bl_description = "Sign the scalar value"
|
||||
@property
|
||||
def layer_obj(self):
|
||||
return bpy.data.objects.get(self.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 is_missing_layer(self.layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||
node_name="FetchLayer",
|
||||
node_label="Fetch Layer")
|
||||
node = bpy.context.active_node
|
||||
|
||||
node.inputs[0].default_value = self.layer_obj
|
||||
node.label = get_layer_label(self.layer_obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_selected_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
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 is not None:
|
||||
if layer_filter(layer_obj, context):
|
||||
select_objs.append(layer_obj)
|
||||
|
||||
return list(set(select_objs))
|
||||
|
||||
|
||||
def get_selected_layer(context, layer_filter=None):
|
||||
layer_objs = get_selected_layers(context, layer_filter)
|
||||
return layer_objs[0] if len(layer_objs) > 0 else None
|
||||
|
||||
|
||||
class OutputLayerOperator():
|
||||
new_layer_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
"""do the operation"""
|
||||
return orig_layer
|
||||
|
||||
def add_layer_node(self, context, layer):
|
||||
layer_obj = layer_to_obj(layer,
|
||||
container_obj=context.object,
|
||||
cache_dir=get_cache_dir())
|
||||
|
||||
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
|
||||
layer_obj_name=layer_obj.name)
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
orig_layer = obj_to_layer(layer_obj)
|
||||
try:
|
||||
new_layer = self.operate(orig_layer, context)
|
||||
except NoContent as e:
|
||||
self.report({"WARNING"}, e.message)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class FillOperator(ScalarOperator):
|
||||
class LayerOperator():
|
||||
|
||||
def operate(self, layer_obj: bpy.types.Object, context):
|
||||
"""do the operation"""
|
||||
...
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.operate(layer_obj, context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RetimeLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.retime_layer"
|
||||
bl_label = "Retime Sequence"
|
||||
bl_description = "Retime layer time sequence"
|
||||
bl_icon = "TIME"
|
||||
|
||||
frame_duration: bpy.props.IntProperty(name="Frames") # type: ignore
|
||||
frame_start: bpy.props.IntProperty(name="Start") # type: ignore
|
||||
frame_offset: bpy.props.IntProperty(name="Offset") # type: ignore
|
||||
sequence_mode: bpy.props.EnumProperty(name="Mode",
|
||||
default="REPEAT",
|
||||
items=[("CLIP", "Clip", ""),
|
||||
("EXTEND", "Extend", ""),
|
||||
("REPEAT", "Repeat", ""),
|
||||
("PING_PONG", "Ping-Pong", "")]) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.frame_duration = self.frame_duration
|
||||
layer_obj.data.frame_start = self.frame_start
|
||||
layer_obj.data.frame_offset = self.frame_offset
|
||||
layer_obj.data.sequence_mode = self.sequence_mode
|
||||
|
||||
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'}
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.frame_duration = layer_obj.data.frame_duration
|
||||
self.frame_start = layer_obj.data.frame_start
|
||||
self.frame_offset = layer_obj.data.frame_offset
|
||||
self.sequence_mode = layer_obj.data.sequence_mode
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Retime {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"
|
||||
class RelocateLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.relocate_layer"
|
||||
bl_label = "Relocate Layer Cache"
|
||||
bl_description = "Relocate layer cache"
|
||||
bl_icon = "FILE"
|
||||
|
||||
threshold: bpy.props.FloatProperty(
|
||||
name="Threshold",
|
||||
soft_min=0, soft_max=1024,
|
||||
default=128,
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.filepath = self.filepath
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
context.window_manager.fileselect_add(self)
|
||||
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"
|
||||
|
||||
new_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
name = f"{layer_obj.parent.name}_{self.new_name}"
|
||||
layer_obj.name = name
|
||||
layer_obj.data.name = name
|
||||
|
||||
set_layer_prop_value(layer_obj, "name", self.new_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 == layer_obj:
|
||||
node.label = self.new_name
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.new_name = get_layer_name(layer_obj)
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Rename {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class ResampleLayer(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.resample_layer"
|
||||
bl_label = "Resample Value"
|
||||
bl_description = "Resample value"
|
||||
bl_icon = "ALIASED"
|
||||
|
||||
smooth: bpy.props.IntProperty(name="Smooth Iteration",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
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 operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_shape = self.get_new_shape(new_layer.shape,
|
||||
new_layer.bioxel_size[0],
|
||||
self.bioxel_size)
|
||||
|
||||
new_layer.resize(new_shape, self.smooth)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
|
||||
return new_layer
|
||||
|
||||
def draw(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
orig_shape = get_layer_prop_value(layer_obj, "shape")
|
||||
orig_size = get_layer_prop_value(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, "new_layer_name")
|
||||
layout.prop(self, "bioxel_size")
|
||||
layout.prop(self, "smooth")
|
||||
layout.label(text=layer_shape_text)
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
self.bioxel_size = get_layer_prop_value(layer_obj,
|
||||
"bioxel_size")
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Resample {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class SignScalar(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Value"
|
||||
bl_description = "Sign value"
|
||||
bl_icon = "REMOVE"
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.data = -orig_layer.data
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_Sign"
|
||||
return new_layer
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Sign {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FillOperator(OutputLayerOperator):
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
soft_min=0, soft_max=1024.0,
|
||||
@@ -112,30 +346,52 @@ 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):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
scalar_min = get_layer_prop_value(layer_obj, "min")
|
||||
self.fill_value = min(scalar_min, 0)
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Fill {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
data = np.amax(base_layer.data, -1)
|
||||
|
||||
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 operate(self, orig_layer: Layer, context):
|
||||
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.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{base_layer.name}_{self.threshold}-Filled"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
|
||||
return {'FINISHED'}
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.threshold:.2f}"
|
||||
return new_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_description = "Fill value by range"
|
||||
bl_icon = "IPO_CONSTANT"
|
||||
|
||||
from_min: bpy.props.FloatProperty(
|
||||
name="From Min",
|
||||
@@ -149,102 +405,200 @@ 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 operate(self, orig_layer: Layer, context):
|
||||
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.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'}
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.from_min:.2f}-{self.from_max:.2f}"
|
||||
return new_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_description = "Fill value by label"
|
||||
bl_icon = "MESH_CAPSULE"
|
||||
|
||||
label_obj_name: bpy.props.EnumProperty(
|
||||
name="Label Layer",
|
||||
items=get_label_layer_selection
|
||||
) # type: ignore
|
||||
smooth: bpy.props.IntProperty(name="Smooth Iteration",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
soft_min=0, soft_max=1024.0,
|
||||
default=0,
|
||||
) # type: ignore
|
||||
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
|
||||
items=get_label_layer_selection) # type: ignore
|
||||
|
||||
invert: bpy.props.BoolProperty(
|
||||
name="Invert Label",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
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)
|
||||
if label_obj is None:
|
||||
raise NoContent("Cannot find any label layer.")
|
||||
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(base_layer.shape)
|
||||
label_layer.resize(orig_layer.shape, self.smooth)
|
||||
mask = np.amax(label_layer.data, -1)
|
||||
if self.invert:
|
||||
mask = 1 - mask
|
||||
|
||||
modified_layer = base_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'}
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{label_layer.name}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||
class CombineLabels(bpy.types.Operator, OutputLayerOperator):
|
||||
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
|
||||
if get_layer_prop_value(obj, "kind") == "label"]
|
||||
def layer_filter(layer_obj, context):
|
||||
return get_layer_kind(layer_obj) == "label"
|
||||
|
||||
label_objs = get_selected_layers(context, layer_filter)
|
||||
|
||||
if len(label_objs) < 2:
|
||||
self.report({"WARNING"}, "Not enough layers.")
|
||||
return {'FINISHED'}
|
||||
|
||||
base_obj = label_objs[0]
|
||||
label_objs = label_objs[1:]
|
||||
|
||||
base_layer = obj_to_layer(base_obj)
|
||||
modified_layer = base_layer.copy()
|
||||
new_layer = base_layer.copy()
|
||||
label_names = [base_layer.name]
|
||||
|
||||
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)
|
||||
new_layer.data = np.maximum(
|
||||
new_layer.data, label_layer.data)
|
||||
label_names.append(label_layer.name)
|
||||
|
||||
modified_layer.name = f"{'-'.join(label_names)}-Combined"
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"C-{'-'.join(label_names)}"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BatchLayerOperator():
|
||||
success_msg = "Successfully done selected layers."
|
||||
fail_msg = "fails."
|
||||
|
||||
def execute(self, context):
|
||||
layer_objs = self.get_layers(context)
|
||||
|
||||
fails = []
|
||||
for layer_obj in layer_objs:
|
||||
try:
|
||||
self.operate(layer_obj, context)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, self.success_msg)
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} {self.fail_msg}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveLayersCache(BatchLayerOperator):
|
||||
fail_msg = "fail to save."
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
output_dir = bpy.path.abspath(self.cache_dir)
|
||||
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = layer_obj.data.is_sequence
|
||||
|
||||
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))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class RemoveLayers(BatchLayerOperator):
|
||||
fail_msg = "fail to remove."
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
for node_group in bpy.data.node_groups:
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node_group.nodes.remove(node)
|
||||
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
|
||||
if cache_filepath.is_file():
|
||||
if 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(layer_obj.data)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove them?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class SaveSelectedLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_selected_layers_cache"
|
||||
bl_label = "Save Selected Layers Cache"
|
||||
bl_description = "Save selected layers' Cache"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all selected layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_selected_layers(context, is_not_missing)
|
||||
|
||||
|
||||
class RemoveSelectedLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_selected_layers"
|
||||
bl_label = "Remove Selected Layers"
|
||||
bl_description = "Remove selected layers"
|
||||
bl_icon = "TRASH"
|
||||
|
||||
success_msg = "Successfully removed all selected layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
return get_selected_layers(context)
|
||||
|
||||
@@ -1,81 +1,51 @@
|
||||
import re
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ..bioxelutils.common import get_all_layer_objs, is_missing_layer, set_file_prop
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
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
|
||||
|
||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
class ReLinkNodes(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_nodes"
|
||||
bl_label = "Relink Nodes to Addon"
|
||||
bl_description = "Relink all nodes to addon source"
|
||||
class ReLinkNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_node_lib"
|
||||
bl_label = "Relink Node Library"
|
||||
bl_description = "Relink all nodes to addon library source"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
node_lib_filename: bpy.props.StringProperty(
|
||||
default=""
|
||||
) # type: ignore
|
||||
|
||||
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
|
||||
lib_filepath = Path(NODE_LIB_FILEPATH.parent,
|
||||
f"{self.node_lib_filename}.blend")
|
||||
node_libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_lib = node_group.library
|
||||
if node_lib:
|
||||
node_libs.append(node_lib)
|
||||
|
||||
node_libs = list(set(node_libs))
|
||||
|
||||
for node_lib in node_libs:
|
||||
node_lib.filepath = str(lib_filepath)
|
||||
# FIXME: may cause crash
|
||||
node_lib.reload()
|
||||
|
||||
self.report({"INFO"}, f"Successfully relinked.")
|
||||
|
||||
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"
|
||||
bl_description = "Save all staged data in this file for sharing"
|
||||
|
||||
save_cache: bpy.props.BoolProperty(
|
||||
name="Save Layer Caches",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
save_lib: bpy.props.BoolProperty(
|
||||
name="Save Node Library File",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
class SaveNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_node_lib"
|
||||
bl_label = "Save Node Library"
|
||||
bl_description = "Save node library file to local"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
lib_dir: bpy.props.StringProperty(
|
||||
name="Library Directory",
|
||||
@@ -84,132 +54,185 @@ class SaveStagedData(bpy.types.Operator):
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.save_lib:
|
||||
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))
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
local_lib_path: Path = Path(lib_dir, NODE_LIB_FILEPATH.name).resolve()
|
||||
addon_lib_path: Path = NODE_LIB_FILEPATH
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
for file in files:
|
||||
file_name = Path(file).name
|
||||
# "//"
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
if local_lib_path != addon_lib_path:
|
||||
shutil.copy(addon_lib_path, local_lib_path)
|
||||
|
||||
output_path: Path = Path(lib_dir, file_name).resolve()
|
||||
source_path: Path = Path(file).resolve()
|
||||
libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
if node_group.library:
|
||||
libs.append(node_group.library)
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
libs = list(set(libs))
|
||||
for lib in libs:
|
||||
lib.filepath = bpy.path.relpath(str(local_lib_path),
|
||||
start=str(blend_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}")
|
||||
|
||||
if self.save_cache:
|
||||
fails = []
|
||||
for layer in get_all_layer_objs():
|
||||
try:
|
||||
save_layer_cache(layer, self.cache_dir)
|
||||
except:
|
||||
fails.append(layer)
|
||||
|
||||
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.")
|
||||
set_file_prop("addon_version", VERSION)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
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, "save_cache")
|
||||
panel.prop(self, "cache_dir")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_lib")
|
||||
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
|
||||
class CleanTemp(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_temp"
|
||||
bl_label = "Clean Temp"
|
||||
bl_description = "Clean all cache in temp (include other project cache)"
|
||||
|
||||
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"
|
||||
bl_description = "Clean all caches saved in temp"
|
||||
|
||||
def execute(self, context):
|
||||
cache_dir = get_cache_dir(context)
|
||||
cache_dir = get_cache_dir()
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
self.report({"INFO"}, f"Successfully cleaned caches.")
|
||||
self.report({"INFO"}, f"Successfully cleaned temp.")
|
||||
return {'FINISHED'}
|
||||
except:
|
||||
self.report({"WARNING"},
|
||||
"Fail to clean caches, you may do it manually.")
|
||||
"Fail to clean temp, you may do it manually.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message="All caches will be cleaned, include other project files, do you still want to clean?")
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class RenderSettingPreset(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.render_setting_preset"
|
||||
bl_label = "Render Setting Presets"
|
||||
bl_description = "Render setting presets for bioxel"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
PRESETS = {
|
||||
"preview_e": "Preview (EEVEE)",
|
||||
"preview_c": "Preview (Cycles)",
|
||||
"production_e": "Production (EEVEE)",
|
||||
"production_c": "Production (Cycles)"
|
||||
}
|
||||
|
||||
preset: bpy.props.EnumProperty(name="Preset",
|
||||
default="preview_c",
|
||||
items=[(k, v, "")
|
||||
for k, v in PRESETS.items()]) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.preset == "preview_e":
|
||||
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 == "production_e":
|
||||
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 == "preview_c":
|
||||
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 = 1
|
||||
bpy.context.scene.cycles.volume_step_rate = 1
|
||||
# bpy.context.scene.cycles.use_fast_gi = True
|
||||
|
||||
elif self.preset == "production_c":
|
||||
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 = 0.5
|
||||
bpy.context.scene.cycles.volume_step_rate = 0.5
|
||||
# bpy.context.scene.cycles.use_fast_gi = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SliceViewer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.slice_viewer"
|
||||
bl_label = "Slice Viewer"
|
||||
bl_description = "A preview scene setting for viewing slicers"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def execute(self, context):
|
||||
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
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
area.spaces[0].shading.type = 'MATERIAL'
|
||||
area.spaces[0].shading.studio_light = 'studio.exr'
|
||||
area.spaces[0].shading.studiolight_intensity = 2.5
|
||||
area.spaces[0].shading.use_scene_lights = False
|
||||
area.spaces[0].shading.use_scene_world = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_all_layers(layer_filter=None):
|
||||
def _layer_filter(layer_obj):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
layer_objs = get_all_layer_objs()
|
||||
return [obj for obj in layer_objs if layer_filter(obj)]
|
||||
|
||||
|
||||
class SaveAllLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_all_layers_cache"
|
||||
bl_label = "Save All Layers Cache"
|
||||
bl_description = "Save all cache of this file"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_not_missing)
|
||||
|
||||
|
||||
class RemoveAllMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_all_missing_layers"
|
||||
bl_label = "Remove All Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_missing)
|
||||
|
||||
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'}
|
||||
|
||||
62
bioxelnodes/operators/node.py
Normal file
62
bioxelnodes/operators/node.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import bpy
|
||||
|
||||
from ..bioxelutils.common import get_file_prop, is_incompatible, local_lib_not_updated
|
||||
from ..constants import VERSION
|
||||
from ast import literal_eval
|
||||
from ..bioxelutils.node import assign_node_group, get_node_group
|
||||
from ..utils import get_use_link
|
||||
|
||||
|
||||
class AddNode(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_node"
|
||||
bl_label = "Add Node"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
node_name: bpy.props.StringProperty(
|
||||
default='',
|
||||
) # type: ignore
|
||||
|
||||
node_label: bpy.props.StringProperty(
|
||||
default=''
|
||||
) # type: ignore
|
||||
|
||||
node_description: bpy.props.StringProperty(
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
@property
|
||||
def node_type(self):
|
||||
return f"BioxelNodes_{self.node_name}"
|
||||
|
||||
def execute(self, context):
|
||||
space = context.space_data
|
||||
if space.type != "NODE_EDITOR":
|
||||
self.report({"ERROR"}, "Not in node editor.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not space.edit_tree.is_editable:
|
||||
self.report({"ERROR"}, "Not editable.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if is_incompatible():
|
||||
self.report({"ERROR"},
|
||||
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
get_node_group(self.node_type, get_use_link())
|
||||
bpy.ops.node.add_node(
|
||||
'INVOKE_DEFAULT',
|
||||
type='GeometryNodeGroup',
|
||||
use_transform=True
|
||||
)
|
||||
node = bpy.context.active_node
|
||||
assign_node_group(node, self.node_type)
|
||||
node.label = self.node_label
|
||||
|
||||
node.show_options = False
|
||||
|
||||
if local_lib_not_updated():
|
||||
self.report({"WARNING"},
|
||||
"Local library version does not match the current addon version, which may cause problems, please save the node library again.")
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -1,60 +0,0 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
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)
|
||||
|
||||
target_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
|
||||
|
||||
def progress_bar(self, context):
|
||||
row = self.layout.row()
|
||||
row.progress(
|
||||
factor=context.window_manager.bioxelnodes_progress_factor,
|
||||
type="BAR",
|
||||
text=context.window_manager.bioxelnodes_progress_text
|
||||
)
|
||||
row.scale_x = 2
|
||||
|
||||
|
||||
def progress_update(context, factor, text=""):
|
||||
context.window_manager.bioxelnodes_progress_factor = factor
|
||||
context.window_manager.bioxelnodes_progress_text = text
|
||||
|
||||
|
||||
def get_preferences(context):
|
||||
return context.preferences.addons[base_package].preferences
|
||||
|
||||
|
||||
def get_cache_dir(context):
|
||||
preferences = get_preferences(context)
|
||||
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
return str(cache_path)
|
||||
@@ -1,6 +1,7 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
@@ -10,14 +11,19 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||
) # type: ignore
|
||||
|
||||
do_change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting",
|
||||
change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting on First Import",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
node_import_method: bpy.props.EnumProperty(name="Node Import Method",
|
||||
default="LINK",
|
||||
items=[("LINK", "Link", ""),
|
||||
("APPEND", "Append (Reuse Data)", "")]) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
layout.prop(self, "do_change_render_setting")
|
||||
layout.prop(self, "change_render_setting")
|
||||
layout.prop(self, "node_import_method")
|
||||
|
||||
@@ -1,5 +1,44 @@
|
||||
import bpy
|
||||
|
||||
from .bioxelutils.common 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
|
||||
|
||||
layer_list_UL.layer_list_active = -1
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import bpy
|
||||
|
||||
|
||||
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
@@ -30,3 +31,42 @@ def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
|
||||
if not target_path.exists():
|
||||
raise Exception
|
||||
|
||||
|
||||
def select_object(target_obj):
|
||||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
target_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
|
||||
|
||||
def progress_bar(self, context):
|
||||
row = self.layout.row()
|
||||
row.progress(
|
||||
factor=context.window_manager.bioxelnodes_progress_factor,
|
||||
type="BAR",
|
||||
text=context.window_manager.bioxelnodes_progress_text
|
||||
)
|
||||
row.scale_x = 2
|
||||
|
||||
|
||||
def progress_update(context, factor, text=""):
|
||||
context.window_manager.bioxelnodes_progress_factor = factor
|
||||
context.window_manager.bioxelnodes_progress_text = text
|
||||
|
||||
|
||||
def get_preferences():
|
||||
return bpy.context.preferences.addons[__package__].preferences
|
||||
|
||||
|
||||
def get_cache_dir():
|
||||
preferences = get_preferences()
|
||||
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
return str(cache_path)
|
||||
|
||||
|
||||
def get_use_link():
|
||||
preferences = get_preferences()
|
||||
return preferences.node_import_method == "LINK"
|
||||
|
||||
3
build.py
3
build.py
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "bioxelnodes"
|
||||
version = "0.3.3"
|
||||
version = "1.0.0"
|
||||
description = ""
|
||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user