feat: improving the user experience

This commit is contained in:
2024-08-20 14:21:10 +08:00
parent 051df670a5
commit 655b19bc7c
29 changed files with 1636 additions and 1167 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -2,7 +2,6 @@ import copy
import numpy as np
from . import scipy
from . import skimage as ski
from . import scipy as ndi
# 3rd-party
@@ -70,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,)
@@ -97,12 +100,11 @@ 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']:
@@ -121,15 +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, :, :, :, :],
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)

View File

@@ -1,6 +1,5 @@
from pathlib import Path
import numpy as np
from .layer import Layer
# 3rd-party
import SimpleITK as sitk
@@ -166,7 +165,7 @@ def remove_end_str(string: str, end: str):
return string
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None) -> Layer:
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:

View File

@@ -1,5 +1,10 @@
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]
@@ -48,20 +53,65 @@ def get_container_obj(current_obj):
return None
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
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]
value = layer_node.inputs[prop].default_value
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)
if type(value).__name__ == "float":
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):
@@ -95,6 +145,7 @@ def get_all_layer_objs():
return layer_objs
def add_driver(target, target_prop, var_sources, expression):
driver = target.driver_add(target_prop)
is_vector = isinstance(driver, list)
@@ -125,4 +176,70 @@ def add_direct_driver(target, target_prop, source, source_prop):
}
]
expression = "var0"
add_driver(target, target_prop, drivers, expression)
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

View File

@@ -6,12 +6,12 @@ from mathutils import Matrix, Vector
from .layer import Layer, layer_to_obj, obj_to_layer
from ..nodes import custom_nodes
from .utils import (get_container_layer_objs,
get_layer_prop_value,
get_nodes_by_type,
move_node_to_node)
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",
@@ -82,13 +82,17 @@ def add_layers(layers: list[Layer],
cache_dir: str):
node_group = container_obj.modifiers[0].node_group
output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0]
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 = custom_nodes.add_node(node_group,
"BioxelNodes_FetchLayer")
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
@@ -104,6 +108,7 @@ def add_layers(layers: list[Layer],
def container_to_obj(container: Container,
scene_scale: float,
step_size: float,
cache_dir: str):
# Wrapper a Container
@@ -134,11 +139,26 @@ 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
modifier = container_obj.modifiers.new("GeometryNodes", 'NODES')
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
node_group.interface.new_socket(name="Component",
node_group.interface.new_socket(name="Output",
in_out="OUTPUT",
socket_type="NodeSocketGeometry")
modifier.node_group = node_group

View File

@@ -7,10 +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 .utils import (get_layer_prop_value,
move_node_between_nodes, add_direct_driver)
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):
@@ -26,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)
@@ -34,7 +39,11 @@ 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
@@ -137,13 +146,12 @@ def layer_to_obj(layer: Layer,
layer_data = bpy.data.volumes.new(layer_display_name)
layer_data.render.space = 'WORLD'
layer_data.render.step_size = container_obj.scale[0] * layer.bioxel_size[0]
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])
# add_direct_driver(layer_data, "render.step_size",
# container_obj, "scale[0]")
if layer.frame_count > 1:
layer_data.is_sequence = True
layer_data.frame_duration = layer.frame_count
@@ -164,8 +172,7 @@ def layer_to_obj(layer: Layer,
socket_type="NodeSocketGeometry")
modifier.node_group = node_group
layer_node = custom_nodes.add_node(node_group,
"BioxelNodes__Layer")
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
@@ -176,11 +183,12 @@ 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

View File

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

View File

@@ -1,7 +1,7 @@
schema_version = "1.0.0"
id = "bioxelnodes"
version = "0.4.0"
version = "1.0.0"
name = "Bioxel Nodes"
tagline = "For scientific volumetric data visualization in Blender"
maintainer = "Ma Nan <icrdr2010@outlook.com>"

View File

@@ -1,8 +1,9 @@
from pathlib import Path
from .customnodes import CustomNodes
NODE_FILE = "BioxelNodes_4.2"
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 = [
{
@@ -12,34 +13,40 @@ MENU_ITEMS = [
{
'label': 'Cutout by Threshold',
'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_CutoutByThreshold',
'node_description': ''
'name': 'CutoutByThreshold',
'description': ''
},
{
'label': 'Cutout by Range',
'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_CutoutByRange',
'node_description': ''
'name': 'CutoutByRange',
'description': ''
},
{
'label': 'Cutout by Color',
'icon': 'COLOR',
'node_type': 'BioxelNodes_CutoutByColor',
'node_description': ''
'name': 'CutoutByColor',
'description': ''
},
"separator",
{
'label': 'To Surface',
'icon': 'MESH_DATA',
'node_type': 'BioxelNodes_ToSurface',
'node_description': ''
'name': 'ToSurface',
'description': ''
},
{
'label': 'Join Component',
'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_JoinComponent',
'node_description': ''
'name': 'JoinComponent',
'description': ''
},
{
'label': 'Slice',
'icon': 'TEXTURE',
'name': 'Slice',
'description': ''
}
]
},
{
@@ -49,59 +56,46 @@ MENU_ITEMS = [
{
'label': 'Set Properties',
'icon': 'PROPERTIES',
'node_type': 'BioxelNodes_SetProperties',
'node_description': ''
'name': 'SetProperties',
'description': ''
},
"separator",
{
'label': 'Set Color',
'icon': 'IPO_SINE',
'node_type': 'BioxelNodes_SetColor',
'node_description': ''
'name': 'SetColor',
'description': ''
},
{
'label': 'Set Color by Color',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByColor',
'node_description': ''
'name': 'SetColorByColor',
'description': ''
},
{
'label': 'Set Color by Ramp 2',
'icon': 'IPO_QUAD',
'node_type': 'BioxelNodes_SetColorByRamp2',
'node_description': ''
'name': 'SetColorByRamp2',
'description': ''
},
{
'label': 'Set Color by Ramp 3',
'icon': 'IPO_CUBIC',
'node_type': 'BioxelNodes_SetColorByRamp3',
'node_description': ''
'name': 'SetColorByRamp3',
'description': ''
},
{
'label': 'Set Color by Ramp 4',
'icon': 'IPO_QUART',
'node_type': 'BioxelNodes_SetColorByRamp4',
'node_description': ''
'name': 'SetColorByRamp4',
'description': ''
},
{
'label': 'Set Color by Ramp 5',
'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByRamp5',
'node_description': ''
},
# "separator",
# {
# 'label': 'Color Presets',
# 'icon': 'COLOR',
# 'node_type': 'BioxelNodes_ColorPresets',
# 'node_description': ''
# }
# {
# 'label': 'Color Presets MRI',
# 'icon': 'COLOR',
# 'node_type': 'BioxelNodes_ColorPresets_MRI',
# 'node_description': ''
# },
'name': 'SetColorByRamp5',
'description': ''
}
]
},
{
@@ -111,39 +105,45 @@ MENU_ITEMS = [
{
'label': 'Membrane Shader',
'icon': 'NODE_MATERIAL',
'node_type': 'BioxelNodes_AssignMembraneShader',
'node_description': ''
'name': 'AssignMembraneShader',
'description': ''
},
{
'label': 'Solid Shader',
'icon': 'SHADING_SOLID',
'node_type': 'BioxelNodes_AssignSolidShader',
'node_description': ''
'name': 'AssignSolidShader',
'description': ''
},
{
'label': 'Slime Shader',
'icon': 'OUTLINER_DATA_META',
'node_type': 'BioxelNodes_AssignSlimeShader',
'node_description': ''
'name': 'AssignSlimeShader',
'description': ''
},
"separator",
]
},
{
'label': 'Transform',
'icon': 'EMPTY_AXIS',
'items': [
{
'label': 'Inflate',
'icon': 'OUTLINER_OB_META',
'node_type': 'BioxelNodes_M_Inflate',
'node_description': ''
'label': 'Transform',
'icon': 'EMPTY_AXIS',
'name': 'Transform',
'description': ''
},
{
'label': 'Smooth',
'icon': 'MOD_SMOOTH',
'node_type': 'BioxelNodes_M_Smooth',
'node_description': ''
'label': 'Transform Parent',
'icon': 'ORIENTATION_PARENT',
'name': 'TransformParent',
'description': ''
},
{
'label': 'Remove Small Island',
'icon': 'FORCE_LENNARDJONES',
'node_type': 'BioxelNodes_M_RemoveSmallIsland',
'node_description': ''
'label': 'ReCenter',
'icon': 'PROP_CON',
'name': 'ReCenter',
'description': ''
}
]
},
@@ -154,21 +154,21 @@ MENU_ITEMS = [
{
'label': 'Cut',
'icon': 'MOD_BEVEL',
'node_type': 'BioxelNodes_Cut',
'node_description': ''
'name': 'Cut',
'description': ''
},
"separator",
{
'label': 'Primitive Cutter',
'icon': 'MOD_LINEART',
'node_type': 'BioxelNodes_PrimitiveCutter',
'node_description': ''
'name': 'PrimitiveCutter',
'description': ''
},
{
'label': 'Object Cutter',
'icon': 'MESH_PLANE',
'node_type': 'BioxelNodes_ObjectCutter',
'node_description': ''
'name': 'ObjectCutter',
'description': ''
}
]
},
@@ -177,41 +177,48 @@ MENU_ITEMS = [
'icon': 'MODIFIER',
'items': [
{
'label': 'Pick Mesh',
'label': 'Pick Surface',
'icon': 'OUTLINER_OB_MESH',
'node_type': 'BioxelNodes_PickMesh',
'node_description': ''
'name': 'PickSurface',
'description': ''
},
{
'label': 'Pick Volume',
'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_PickVolume',
'node_description': ''
'name': 'PickVolume',
'description': ''
},
{
'label': 'Pick Shape Wire',
'icon': 'FILE_VOLUME',
'name': 'PickShapeWire',
'description': ''
},
{
'label': 'Pick Bbox Wire',
'icon': 'MESH_CUBE',
'node_type': 'BioxelNodes_PickBboxWire',
'node_description': ''
'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': ''
}
]
}
]
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()

View File

@@ -1 +0,0 @@
from .menus import CustomNodes

View File

@@ -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

View File

@@ -1,117 +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="",
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:
return node_tree
else:
raise RuntimeError('No custom node found')
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"}

View File

@@ -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

View File

@@ -1,46 +1,70 @@
from pathlib import Path
import bpy
from .operators.utils import get_layer_item_label
from .bioxelutils.utils import (get_container_obj,
get_container_objs_from_selection,
get_container_layer_objs,
get_layer_prop_value)
from .operators.layer import (FetchLayerMenu, FetchLayer,
RemoveLayer, RemoveMissingLayers, RenameLayer, ResampleScalar, SaveLayerCache,
SignScalar, CombineLabels,
FillByLabel, FillByThreshold, FillByRange, get_selected_objs_in_node_tree)
from .operators.container import (SaveAllLayerCaches, SaveContainer, LoadContainer,
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 (AddLocator, AddSlicer, ExtractShapeWire,
SaveContainerLayersCache, RemoveContainerMissingLayers,
SaveContainer, LoadContainer,
AddPieCutter, AddPlaneCutter,
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
PickBboxWire, PickMesh, PickVolume, ScaleContainer)
ExtractBboxWire, ExtractSurface, ExtractVolume, ContainerProps)
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
from .operators.misc import (CleanAllCaches,
ReLinkNodes, RenderSettingPreset, SaveStagedData, SliceViewer)
from .operators.misc import (CleanTemp,
ReLinkNodeLib, RemoveAllMissingLayers, RenderSettingPreset, SaveAllLayersCache, SaveNodeLib, SliceViewer)
class PickFromContainerMenu(bpy.types.Menu):
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 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)
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 Object Cutter"
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):
@@ -60,7 +84,7 @@ class ImportLayerMenu(bpy.types.Menu):
class AddLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_ADDLAYER"
bl_label = "Import Volumetric Data (Add to)"
bl_label = "Import Volumetric Data (Add)"
bl_icon = "FILE_NEW"
def draw(self, context):
@@ -79,31 +103,37 @@ class ModifyLayerMenu(bpy.types.Menu):
bl_icon = "FILE_NEW"
def draw(self, context):
layer_objs = get_selected_objs_in_node_tree(context)
if len(layer_objs) > 0:
active_obj_name = layer_objs[0].name
else:
active_obj_name = ""
layout = self.layout
op = layout.operator(ResampleScalar.bl_idname,
icon=ResampleScalar.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(SignScalar.bl_idname,
icon=SignScalar.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByThreshold.bl_idname,
icon=FillByThreshold.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByRange.bl_idname,
icon=FillByRange.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(FillByLabel.bl_idname,
icon=FillByLabel.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(CombineLabels.bl_idname,
icon=CombineLabels.bl_icon)
op.layer_obj_name = active_obj_name
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()
for version in versions:
op = layout.operator(ReLinkNodeLib.bl_idname,
text=version["label"])
op.node_lib_filename = version["filename"]
class RenderSettingMenu(bpy.types.Menu):
@@ -118,6 +148,15 @@ class RenderSettingMenu(bpy.types.Menu):
op.preset = k
class DangerZoneMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_DANGER"
bl_label = "Danger Zone"
def draw(self, context):
layout = self.layout
layout.operator(CleanTemp.bl_idname)
class BioxelNodesTopbarMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_TOPBAR"
bl_label = "Bioxel Nodes"
@@ -130,9 +169,15 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
layout.operator(LoadContainer.bl_idname)
layout.separator()
layout.operator(SaveStagedData.bl_idname)
layout.operator(ReLinkNodes.bl_idname)
layout.operator(CleanAllCaches.bl_idname)
layout.operator(SaveAllLayersCache.bl_idname)
layout.operator(RemoveAllMissingLayers.bl_idname)
layout.separator()
layout.operator(SaveNodeLib.bl_idname)
layout.menu(ReLinkNodeLibMenu.bl_idname)
layout.separator()
layout.menu(DangerZoneMenu.bl_idname)
layout.separator()
layout.menu(RenderSettingMenu.bl_idname)
@@ -143,51 +188,66 @@ def TOPBAR(self, context):
layout.menu(BioxelNodesTopbarMenu.bl_idname)
class BioxelNodesNodeMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_NODE"
class NodeHeadMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_NODE_HEAD"
bl_label = "Bioxel Nodes"
bl_icon = "FILE_VOLUME"
def draw(self, context):
layer_objs = get_selected_objs_in_node_tree(context)
if len(layer_objs) > 0:
active_obj_name = layer_objs[0].name
else:
active_obj_name = ""
layout = self.layout
layout.separator()
layout.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon)
layout.separator()
layout.operator(SaveContainer.bl_idname)
layout.separator()
layout.operator(ScaleContainer.bl_idname)
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.menu(PickFromContainerMenu.bl_idname)
layout.operator(SaveAllLayerCaches.bl_idname,
icon=SaveAllLayerCaches.bl_icon)
layout.operator(RemoveMissingLayers.bl_idname,
icon=RemoveMissingLayers.bl_icon)
layout.separator()
layout.menu(FetchLayerMenu.bl_idname)
op = layout.operator(SaveLayerCache.bl_idname,
icon=SaveLayerCache.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(RenameLayer.bl_idname,
icon=RenameLayer.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(RemoveLayer.bl_idname,
icon=RemoveLayer.bl_icon)
op.layer_obj_name = active_obj_name
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.menu(ModifyLayerMenu.bl_idname)
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(self, context):
def NODE_CONTEXT(self, context):
container_obj = context.object
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
is_container = get_container_obj(container_obj)
@@ -197,10 +257,23 @@ def NODE(self, context):
layout = self.layout
layout.separator()
layout.menu(BioxelNodesNodeMenu.bl_idname)
layout.menu(NodeContextMenu.bl_idname)
def NODE_PT(self, context):
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)
@@ -216,20 +289,19 @@ def NODE_PT(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
layer_list.clear()
for layer_obj in get_container_layer_objs(container_obj):
layer_item = layer_list.add()
layer_item.label = get_layer_item_label(context, layer_obj)
layer_item.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", "min", "max", ]])
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list):
active_obj_name = layer_list[layer_list_active].obj_name
else:
active_obj_name = ""
for prop in ["kind",
"bioxel_size",
"shape",
"frame_count",
"channel_count",
"min", "max"]])
layout = self.layout
layout.label(text="Layer List")
@@ -243,48 +315,41 @@ def NODE_PT(self, context):
item_dyntip_propname="info_text",
rows=20)
sidebar = split.column(align=True)
sidebar.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon, text="")
# sidebar = split.column(align=True)
# sidebar.menu(AddLayerMenu.bl_idname,
# icon=AddLayerMenu.bl_icon, text="")
sidebar.operator(SaveAllLayerCaches.bl_idname,
icon=SaveAllLayerCaches.bl_icon, text="")
sidebar.operator(RemoveMissingLayers.bl_idname,
icon=RemoveMissingLayers.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()
op = sidebar.operator(FetchLayer.bl_idname,
icon=FetchLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(SaveLayerCache.bl_idname,
icon=SaveLayerCache.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(RenameLayer.bl_idname,
icon=RenameLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(RemoveLayer.bl_idname,
icon=RemoveLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name
# sidebar.separator()
# sidebar.operator(SaveSelectedLayersCache.bl_idname,
# icon=SaveSelectedLayersCache.bl_icon, text="")
# sidebar.operator(RemoveSelectedLayers.bl_idname,
# icon=RemoveSelectedLayers.bl_icon, text="")
sidebar.separator()
op = sidebar.operator(ResampleScalar.bl_idname,
icon=ResampleScalar.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(SignScalar.bl_idname,
icon=SignScalar.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByThreshold.bl_idname,
icon=FillByThreshold.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByRange.bl_idname,
icon=FillByRange.bl_icon, text="")
op.layer_obj_name = active_obj_name
op = sidebar.operator(FillByLabel.bl_idname,
icon=FillByLabel.bl_icon, text="")
op.layer_obj_name = active_obj_name
# sidebar.separator()
# 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()
layout.separator()
# 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):
@@ -293,15 +358,24 @@ def VIEW3D_TOPBAR(self, context):
icon=SliceViewer.bl_icon, text="")
node_menu = NodeMenu(
menu_items=MENU_ITEMS
)
def add():
node_menu.register()
bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR)
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PT)
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
bpy.types.NODE_MT_editor_menus.append(NODE)
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.VIEW3D_HT_header.remove(VIEW3D_TOPBAR)
bpy.types.NODE_PT_node_tree_properties.remove(NODE_PT)
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
bpy.types.NODE_MT_editor_menus.remove(NODE)
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
View 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)

View File

@@ -2,17 +2,16 @@ import bpy
import bmesh
from ..nodes import custom_nodes
from ..customnodes.nodes import AddCustomNode
from ..utils import get_cache_dir, get_use_link, select_object
from ..bioxel.io import load_container, save_container
from ..bioxelutils.container import container_to_obj, obj_to_container
from ..bioxelutils.utils import (get_container_layer_objs, get_container_obj,
get_layer_prop_value, get_nodes_by_type,
move_node_between_nodes,
move_node_to_node,
get_all_layer_objs)
from .utils import get_cache_dir, select_object
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):
@@ -63,7 +62,7 @@ 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:
@@ -78,7 +77,7 @@ class LoadContainer(bpy.types.Operator):
return {'RUNNING_MODAL'}
class PickObject():
class ExtractObject():
bl_options = {'UNDO'}
def execute(self, context):
@@ -92,18 +91,19 @@ class PickObject():
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)
@@ -112,26 +112,34 @@ class PickObject():
return {'FINISHED'}
class PickMesh(bpy.types.Operator, PickObject):
bl_idname = "bioxelnodes.pick_mesh"
bl_label = "Pick Mesh"
bl_description = "Pick Container 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 = "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"
@@ -162,14 +170,12 @@ class AddCutter():
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 = bpy.context.active_object
pie.name = "Pie"
orig_data = pie.data
pie_obj = bpy.context.active_object
orig_data = pie_obj.data
# Create mesh
pie_mesh = bpy.data.meshes.new('Pie')
pie.data = pie_mesh
pie_obj.data = pie_mesh
bpy.data.meshes.remove(orig_data)
# # Create object
@@ -204,7 +210,19 @@ class AddCutter():
# bpy.context.view_layer.objects.active = pie
cutter_obj = bpy.context.active_object
cutter_obj.location = container_obj.location
# 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
@@ -213,6 +231,7 @@ 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)
@@ -222,13 +241,16 @@ class AddCutter():
cut_nodes = get_nodes_by_type(node_group,
'BioxelNodes_Cut')
if len(cut_nodes) == 0:
cutter_node = custom_nodes.add_node(node_group,
"BioxelNodes_ObjectCutter")
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 = custom_nodes.add_node(node_group,
'BioxelNodes_Cut')
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:
@@ -249,58 +271,20 @@ class AddCutter():
move_node_to_node(cutter_node, cut_node, (-300, -300))
else:
bpy.ops.bioxelnodes.object_cutter('INVOKE_DEFAULT',
cutter_obj_name=cutter_obj.name,
cutter_type=self.cutter_type.capitalize())
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'}
class ObjectCutter(bpy.types.Operator, AddCustomNode):
bl_idname = "bioxelnodes.object_cutter"
bl_label = "Object Cutter"
bl_description = "Object Cutter"
bl_icon = "NODE"
bl_options = {'UNDO'}
cutter_obj_name: bpy.props.StringProperty() # type: ignore
cutter_type: bpy.props.StringProperty() # type: ignore
def execute(self, context):
cutter_obj = bpy.data.objects.get(self.cutter_obj_name)
if cutter_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
self.get_node_tree(self.node_type, self.node_link)
prev_context = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.ops.node.add_node('INVOKE_DEFAULT',
type='GeometryNodeGroup',
use_transform=True)
bpy.context.area.type = prev_context
node = bpy.context.active_node
self.assign_node_tree(node)
node.show_options = False
node.label = "Object Cutter"
node.inputs[0].default_value = self.cutter_type
node.inputs[1].default_value = cutter_obj
return {"FINISHED"}
def invoke(self, context, event):
self.nodes_file = custom_nodes.nodes_file
self.node_type = "BioxelNodes_ObjectCutter"
return self.execute(context)
class AddPlaneCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_plane_cutter"
bl_label = "Add a Plane Cutter"
bl_description = "Add a Plane Cutter to Container"
bl_description = "Add a plane cutter to current container"
bl_icon = "MESH_PLANE"
cutter_type = "plane"
@@ -308,7 +292,7 @@ class AddPlaneCutter(bpy.types.Operator, AddCutter):
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"
bl_description = "Add a cylinder cutter to current container"
bl_icon = "MESH_CYLINDER"
cutter_type = "cylinder"
@@ -316,7 +300,7 @@ class AddCylinderCutter(bpy.types.Operator, AddCutter):
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"
bl_description = "Add a cube cutter to current container"
bl_icon = "MESH_CUBE"
cutter_type = "cube"
@@ -324,7 +308,7 @@ class AddCubeCutter(bpy.types.Operator, AddCutter):
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"
bl_description = "Add a sphere cutter to current container"
bl_icon = "MESH_UVSPHERE"
cutter_type = "sphere"
@@ -332,15 +316,128 @@ class AddSphereCutter(bpy.types.Operator, AddCutter):
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"
bl_description = "Add a pie cutter to current container"
bl_icon = "MESH_CONE"
cutter_type = "pie"
class ScaleContainer(bpy.types.Operator):
bl_idname = "bioxelnodes.scale_container"
bl_label = "Scale Container"
bl_description = "Scale Container."
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",
@@ -348,6 +445,11 @@ class ScaleContainer(bpy.types.Operator):
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)
@@ -358,10 +460,12 @@ class ScaleContainer(bpy.types.Operator):
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):
bioxel_size = get_layer_prop_value(layer_obj, "bioxel_size")
layer_obj.data.render.step_size = self.scene_scale * bioxel_size
layer_obj.data.render.space = 'WORLD'
layer_obj.data.render.step_size = self.scene_scale * self.step_size
return {'FINISHED'}
@@ -371,49 +475,51 @@ class ScaleContainer(bpy.types.Operator):
if container_obj is None:
return self.execute(context)
else:
self.scene_scale = container_obj.scale[0]
context.window_manager.invoke_props_dialog(self,
width=500)
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'}
class SaveAllLayerCaches(bpy.types.Operator):
bl_idname = "bioxelnodes.save_all_layer_caches"
bl_label = "Save All Layer Caches"
bl_description = "Save Container's caches to directory."
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"
cache_dir: bpy.props.StringProperty(
name="Cache Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
success_msg = "Successfully saved all container layers."
def execute(self, context):
container_obj = get_container_obj(context.object)
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)
if container_obj is None:
self.report({"WARNING"}, "Cannot find any bioxel container.")
return {'FINISHED'}
fails = []
for layer_obj in get_container_layer_objs(container_obj):
try:
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
layer_obj_name=layer_obj.name,
cache_dir=self.cache_dir)
except:
fails.append(layer_obj)
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"
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.")
success_msg = "Successfully removed all container missing layers."
return {'FINISHED'}
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_props_dialog(self,
width=500)
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to remove all **Missing** layers?")
return {'RUNNING_MODAL'}

View File

@@ -7,9 +7,8 @@ from pathlib import Path
from ..exceptions import CancelledByUser
from ..props import BIOXELNODES_Series
from ..bioxelutils.utils import (get_all_layer_objs, get_container_obj,
get_layer_obj,
get_container_objs_from_selection)
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)
@@ -17,8 +16,9 @@ from ..bioxel.layer import Layer
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
get_ext, parse_volumetric_data)
from .utils import (get_cache_dir,
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()
@@ -83,7 +83,7 @@ class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Scalar"
bl_description = "Import Volumetric Data to Container as Scalar"
bl_icon = "EVENT_S"
read_as = "scalar"
read_as = "SCALAR"
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
@@ -91,7 +91,7 @@ class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_L"
read_as = "label"
read_as = "LABEL"
class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
@@ -99,7 +99,7 @@ class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_C"
read_as = "color"
read_as = "COLOR"
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
@@ -142,6 +142,8 @@ class ParseVolumetricData(bpy.types.Operator):
bl_options = {'UNDO'}
meta = None
label_count = 0
dtype = None
thread = None
_timer = None
@@ -160,10 +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", ""),
("color", "Color", "")]) # 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
@@ -172,6 +174,11 @@ 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'}
@@ -205,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
@@ -270,26 +279,49 @@ 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']
if orig_spacing[2] == 1 and orig_spacing[0] < 0.1:
spacing_log10 = math.floor(math.log10(min(*orig_spacing)))
orig_spacing = (orig_spacing[0] * math.pow(10, -spacing_log10-1),
orig_spacing[1] * math.pow(10, -spacing_log10-1),
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))
min_size = min(orig_spacing[0],
orig_spacing[1], orig_spacing[2])
bioxel_size = max(min_size, 1.0)
bioxel_size = max(min(*orig_spacing), 1.0)
# layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
# layer_size = get_layer_size(layer_shape,
# bioxel_size)
# log10 = math.floor(math.log10(max(*layer_size)))
# log10 = max(1, log10)
# log10 = min(3, log10)
# scene_scale = math.pow(10, -log10)
scene_scale = 0.01
layer_shape = get_layer_shape(bioxel_size,
orig_shape,
orig_spacing)
layer_size = get_layer_size(layer_shape,
bioxel_size,
0.01)
min_log10 = math.floor(math.log10(min(*layer_size)))
max_log10 = math.floor(math.log10(max(*layer_size)))
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
@@ -313,6 +345,7 @@ class ParseVolumetricData(bpy.types.Operator):
channel_count=self.meta['channel_count'],
container_obj_name=container_obj_name,
read_as=self.read_as,
label_count=self.label_count,
scene_scale=scene_scale
)
@@ -320,6 +353,8 @@ class ParseVolumetricData(bpy.types.Operator):
return {'FINISHED'}
def invoke(self, context, event):
# 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'}
@@ -420,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
@@ -460,11 +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", ""),
("color", "Color", "")]) # 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,
@@ -482,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):
@@ -516,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)
@@ -551,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 = []
@@ -590,11 +631,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
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
@@ -605,8 +648,6 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
# If all values are the same, the normalized array will be all zeros
data = np.zeros_like(data, dtype=np.float32)
# Convert the normalized array to float dtype
data = data.astype(np.float32)
else:
data = data.astype(np.float32)
@@ -650,7 +691,21 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
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):
@@ -767,15 +822,17 @@ 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:
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)
@@ -806,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):

View File

@@ -1,19 +1,16 @@
from pathlib import Path
import bpy
import re
import numpy as np
from ..exceptions import NoContent
from ..bioxel.layer import Layer
from ..utils import copy_to_dir
from ..customnodes.nodes import AddCustomNode
from ..bioxelutils.utils import (get_container_obj,
get_layer_prop_value,
get_container_layer_objs,
get_node_type, set_layer_prop_value)
from ..bioxelutils.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, get_layer_item_label, get_layer_label, get_preferences
from ..nodes import custom_nodes
from ..utils import get_cache_dir, copy_to_dir
def get_label_layer_selection(self, context):
@@ -21,134 +18,244 @@ 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
def get_selected_objs_in_node_tree(context):
select_objs = []
# node_group = context.space_data.edit_tree
for node in context.selected_nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
layer_obj = node.inputs[0].default_value
if layer_obj != None:
cache_filepath = Path(bpy.path.abspath(
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
select_objs.append(layer_obj)
return select_objs
class LayerOperator():
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'}
layer_obj_name: bpy.props.StringProperty(
options={"HIDDEN"}) # type: ignore
@classmethod
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"]])
@property
def layer_obj(self):
return bpy.data.objects.get(self.layer_obj_name)
@property
def is_lost(self):
if self.layer_obj is None:
return None
cache_filepath = Path(bpy.path.abspath(
self.layer_obj.data.filepath)).resolve()
return not cache_filepath.is_file()
class FetchLayer(bpy.types.Operator, LayerOperator, AddCustomNode):
bl_idname = "bioxelnodes.fetch_layer"
bl_label = "Fetch Layer"
bl_description = "Fetch Layer"
bl_icon = "NODE"
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
if is_missing_layer(self.layer_obj):
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
self.get_node_tree(self.node_type, self.node_link)
prev_context = bpy.context.area.type
bpy.context.area.type = 'NODE_EDITOR'
bpy.ops.node.add_node('INVOKE_DEFAULT',
type='GeometryNodeGroup',
use_transform=True)
bpy.context.area.type = prev_context
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
node_name="FetchLayer",
node_label="Fetch Layer")
node = bpy.context.active_node
self.assign_node_tree(node)
node.show_options = False
layer_obj = self.layer_obj
node.inputs[0].default_value = layer_obj
node.label = get_layer_label(layer_obj)
node.inputs[0].default_value = self.layer_obj
node.label = get_layer_label(self.layer_obj)
return {"FINISHED"}
def invoke(self, context, event):
self.nodes_file = custom_nodes.nodes_file
self.node_type = "BioxelNodes_FetchLayer"
return self.execute(context)
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))
class FetchLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_ADD_LAYER"
bl_label = "Fetch Layer"
def draw(self, context):
container_obj = get_container_obj(bpy.context.active_object)
layer_objs = get_container_layer_objs(container_obj)
layout = self.layout
for layer_obj in layer_objs:
op = layout.operator(FetchLayer.bl_idname,
text=get_layer_item_label(context, layer_obj))
op.layer_obj_name = layer_obj.name
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 ModifyLayerOperator(LayerOperator):
def layer_operate(self, orig_layer: Layer):
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=self.layer_obj.parent,
cache_dir=get_cache_dir(context))
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):
if self.layer_obj == None:
layer_obj = get_selected_layer(context)
if layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
if is_missing_layer(layer_obj):
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
orig_layer = obj_to_layer(self.layer_obj)
new_layer = self.layer_operate(orig_layer)
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 ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
bl_idname = "bioxelnodes.resample_scalar"
bl_label = "Resample Scalar"
bl_description = "Resample Scalar"
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):
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 RelocateLayer(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.relocate_layer"
bl_label = "Relocate Layer Cache"
bl_description = "Relocate layer cache"
bl_icon = "FILE"
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,
@@ -161,19 +268,21 @@ class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
int(orig_shape[1]*orig_size/new_size),
int(orig_shape[2]*orig_size/new_size))
def layer_operate(self, orig_layer: Layer):
modified_layer = orig_layer.copy()
new_shape = self.get_new_shape(modified_layer.shape,
modified_layer.bioxel_size[0],
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)
modified_layer.resize(new_shape)
modified_layer.name = f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
return modified_layer
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):
orig_shape = get_layer_prop_value(self.layer_obj, "shape")
orig_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
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)
@@ -186,45 +295,49 @@ class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
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):
if self.layer_obj:
bioxel_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
self.bioxel_size = bioxel_size
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,
width=400,
title=f"Resample {self.layer_obj.name}")
title=f"Resample {name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class SignScalar(bpy.types.Operator, ModifyLayerOperator):
class SignScalar(bpy.types.Operator, OutputLayerOperator):
bl_idname = "bioxelnodes.sign_scalar"
bl_label = "Sign Scalar"
bl_description = "Sign the scalar value"
bl_label = "Sign Value"
bl_description = "Sign value"
bl_icon = "REMOVE"
def layer_operate(self, orig_layer: Layer):
modified_layer = orig_layer.copy()
modified_layer.data = -orig_layer.data
modified_layer.name = f"{orig_layer.name}_Sign"
return modified_layer
def 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):
if self.layer_obj:
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to sign {self.layer_obj.name}?")
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(ModifyLayerOperator):
class FillOperator(OutputLayerOperator):
fill_value: bpy.props.FloatProperty(
name="Fill Value",
@@ -238,13 +351,13 @@ class FillOperator(ModifyLayerOperator):
) # type: ignore
def invoke(self, context, event):
if self.layer_obj:
scalar_min = get_layer_prop_value(self.layer_obj, "min")
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,
width=400,
title=f"Fill {self.layer_obj.name}")
title=f"Fill {name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
@@ -253,7 +366,7 @@ class FillOperator(ModifyLayerOperator):
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_description = "Fill value by threshold"
bl_icon = "EMPTY_SINGLE_ARROW"
threshold: bpy.props.FloatProperty(
@@ -262,21 +375,22 @@ class FillByThreshold(bpy.types.Operator, FillOperator):
default=128,
) # type: ignore
def layer_operate(self, orig_layer: Layer):
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 = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{orig_layer.name}_F-{self.threshold}"
return modified_layer
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(
@@ -291,82 +405,112 @@ class FillByRange(bpy.types.Operator, FillOperator):
default=256,
) # type: ignore
def layer_operate(self, orig_layer: Layer):
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 = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{orig_layer.name}_F-{self.from_min}-{self.from_max}"
return modified_layer
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"
smooth: bpy.props.IntProperty(name="Smooth Iteration",
default=0,
soft_min=0, soft_max=5,
options={"SKIP_SAVE"}) # type: ignore
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
items=get_label_layer_selection) # type: ignore
def layer_operate(self, orig_layer: Layer):
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'}
if label_obj is None:
raise NoContent("Cannot find any label layer.")
label_layer = obj_to_layer(label_obj)
label_layer.resize(orig_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 = orig_layer.copy()
modified_layer.fill(self.fill_value, mask)
modified_layer.name = f"{orig_layer.name}_F-{label_layer.name}"
return modified_layer
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, ModifyLayerOperator):
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 get_selected_objs_in_node_tree(context)
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"C-{'-'.join(label_names)}"
new_layer.name = self.new_layer_name \
or f"C-{'-'.join(label_names)}"
self.add_layer_node(context, modified_layer)
self.add_layer_node(context, new_layer)
return {'FINISHED'}
class SaveLayerCache(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.save_layer_cache"
bl_label = "Save Layer Cache"
bl_description = "Save Layer Cache"
bl_icon = "FILE_TICK"
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",
@@ -374,23 +518,14 @@ class SaveLayerCache(bpy.types.Operator, LayerOperator):
default="//"
) # type: ignore
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
# "//"
def operate(self, layer_obj, context):
output_dir = bpy.path.abspath(self.cache_dir)
source_dir = bpy.path.abspath(self.layer_obj.data.filepath)
source_dir = bpy.path.abspath(layer_obj.data.filepath)
source_path: Path = Path(source_dir).resolve()
is_sequence = self.layer_obj.data.is_sequence
is_sequence = layer_obj.data.is_sequence
name = self.layer_obj.name if is_sequence else f"{self.layer_obj.name}.vdb"
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()
@@ -401,124 +536,69 @@ class SaveLayerCache(bpy.types.Operator, LayerOperator):
blend_path = Path(bpy.path.abspath("//")).resolve()
self.layer_obj.data.filepath = bpy.path.relpath(str(output_path),
start=str(blend_path))
layer_obj.data.filepath = bpy.path.relpath(str(output_path),
start=str(blend_path))
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'}
else:
return self.execute(context)
context.window_manager.invoke_props_dialog(self)
return {'RUNNING_MODAL'}
class RenameLayer(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.rename_layer"
bl_label = "Rename Layer"
bl_description = "Rename Layer"
bl_icon = "FILE_FONT"
class RemoveLayers(BatchLayerOperator):
fail_msg = "fail to remove."
name: bpy.props.StringProperty(name="New Name") # type: ignore
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
name = f"{self.layer_obj.parent.name}_{self.name}"
self.layer_obj.name = name
self.layer_obj_name = name
self.layer_obj.data.name = name
set_layer_prop_value(self.layer_obj, "name", self.name)
node_group = context.space_data.edit_tree
for node in node_group.nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == self.layer_obj:
node.label = self.name
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
self.name = get_layer_prop_value(self.layer_obj, "name")
context.window_manager.invoke_props_dialog(self,
width=500,
title=f"rename {self.layer_obj.name}")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RemoveLayer(bpy.types.Operator, LayerOperator):
bl_idname = "bioxelnodes.remove_layer"
bl_label = "Remove Selected Layer"
bl_description = "Remove Layer"
bl_icon = "TRASH"
def execute(self, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
node_group = context.space_data.edit_tree
for node in node_group.nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == self.layer_obj:
node_group.nodes.remove(node)
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(
self.layer_obj.data.filepath)).resolve()
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
if self.layer_obj.data.is_sequence:
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(self.layer_obj.data)
bpy.data.volumes.remove(layer_obj.data)
return {'FINISHED'}
def invoke(self, context, event):
if self.layer_obj:
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to remove {self.layer_obj.name}?")
return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RemoveMissingLayers(bpy.types.Operator):
bl_idname = "bioxelnodes.remove_lost_layers"
bl_label = "Remove All Missing Layers"
bl_description = "Remove all missing "
bl_icon = "BRUSH_DATA"
def execute(self, context):
container_obj = context.object
for layer_obj in get_container_layer_objs(container_obj):
cache_filepath = Path(bpy.path.abspath(
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
continue
bpy.ops.bioxelnodes.remove_layer('EXEC_DEFAULT',
layer_obj_name=layer_obj.name)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_confirm(self,
event,
message=f"Are you sure to remove all **Missing** layers?")
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)

View File

@@ -1,54 +1,51 @@
import bpy
from pathlib import Path
import shutil
from .utils import get_cache_dir
from ..nodes import custom_nodes
from ..bioxelutils.utils import (get_container_objs_from_selection,
get_all_layer_objs,
get_container_layer_objs)
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
from ..bioxelutils.common import get_all_layer_objs, is_missing_layer, set_file_prop
from .layer import RemoveLayers, SaveLayersCache
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'}
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",
@@ -57,98 +54,66 @@ 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_obj in get_all_layer_objs():
try:
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
layer_obj_name=layer_obj.name,
cache_dir=self.cache_dir)
except:
fails.append(layer_obj)
if len(fails) == 0:
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj 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 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"
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):
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 Preset"
bl_description = "Render Setting Preset"
bl_label = "Render Setting Presets"
bl_description = "Render setting presets for bioxel"
bl_options = {'UNDO'}
PRESETS = {
"preview_e": "Preview (EEVEE)",
@@ -190,21 +155,24 @@ class RenderSettingPreset(bpy.types.Operator):
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.1
bpy.context.scene.cycles.volume_step_rate = 0.1
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 = "Slice Viewer"
bl_description = "A preview scene setting for viewing slicers"
bl_icon = "FILE_VOLUME"
def execute(self, context):
@@ -215,7 +183,7 @@ class SliceViewer(bpy.types.Operator):
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'
@@ -224,5 +192,47 @@ class SliceViewer(bpy.types.Operator):
area.spaces[0].shading.use_scene_lights = False
area.spaces[0].shading.use_scene_world = False
return {'FINISHED'}
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'}

View 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"}

View File

@@ -1,60 +0,0 @@
import bpy
from pathlib import Path
from ..bioxelutils.utils import get_layer_prop_value
from .. import __package__ as base_package
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)
def get_layer_item_label(context, layer_obj):
label = get_layer_label(layer_obj)
cache_filepath = Path(bpy.path.abspath(layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
cache_dirpath = Path(get_cache_dir(context))
if cache_dirpath in cache_filepath.parents:
label = "* " + label
else:
label = "**MISSING**" + label
return label
def get_layer_label(layer_obj):
name = get_layer_prop_value(layer_obj, "name")
kind = get_layer_prop_value(layer_obj, "kind")
return f"{name}"

View File

@@ -1,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")

View File

@@ -1,6 +1,6 @@
import bpy
from .bioxelutils.utils import get_node_type
from .bioxelutils.common import get_node_type
class BIOXELNODES_UL_layer_list(bpy.types.UIList):
@@ -27,6 +27,8 @@ def select_layer(self, context):
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

View File

@@ -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"