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 import numpy as np
from . import scipy from . import scipy
from . import skimage as ski
from . import scipy as ndi from . import scipy as ndi
# 3rd-party # 3rd-party
@@ -70,23 +69,27 @@ class Layer():
def copy(self): def copy(self):
return copy.deepcopy(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 = () mask_frames = ()
if mask.ndim == 4: if mask.ndim == 4:
if mask.shape[0] != self.frame_count: if mask.shape[0] != self.frame_count:
raise Exception("Mask frame count is not same as ") raise Exception("Mask frame count is not same as ")
for f in range(self.frame_count): for f in range(self.frame_count):
mask_frame = mask[f, :, :, :] mask_frame = mask[f, :, :, :]
mask_frame = scipy.minimum_filter( if smooth > 0:
mask_frame.astype(np.float32), size=3) mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
mode="nearest",
size=smooth)
# mask_frame = scipy.median_filter( # mask_frame = scipy.median_filter(
# mask_frame.astype(np.float32), size=2) # mask_frame.astype(np.float32), size=2)
mask_frames += (mask_frame,) mask_frames += (mask_frame,)
elif mask.ndim == 3: elif mask.ndim == 3:
for f in range(self.frame_count): for f in range(self.frame_count):
mask_frame = mask[:, :, :] mask_frame = mask[:, :, :]
mask_frame = scipy.minimum_filter( if smooth > 0:
mask_frame.astype(np.float32), size=3) mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
mode="nearest",
size=smooth)
# mask_frame = scipy.median_filter( # mask_frame = scipy.median_filter(
# mask_frame.astype(np.float32), size=2) # mask_frame.astype(np.float32), size=2)
mask_frames += (mask_frame,) mask_frames += (mask_frame,)
@@ -97,12 +100,11 @@ class Layer():
_mask = np.expand_dims(_mask, axis=-1) _mask = np.expand_dims(_mask, axis=-1)
self.data = _mask * value + (1-_mask) * self.data 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: if len(shape) != 3:
raise Exception("Shape must be 3 dim") raise Exception("Shape must be 3 dim")
data = self.data data = self.data
order = 0 if self.dtype == bool else 1
# # TXYZC > TXYZ # # TXYZC > TXYZ
# if self.kind in ['label', 'scalar']: # if self.kind in ['label', 'scalar']:
@@ -121,15 +123,22 @@ class Layer():
# shape, # shape,
# preserve_range=True, # preserve_range=True,
# anti_aliasing=data.dtype.kind != "b") # 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) factors = np.divide(self.shape, shape)
zoom_factors = [1 / f for f in factors] 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], zoom_factors+[1.0],
mode="nearest", mode="nearest",
grid_mode=False, grid_mode=False,
order=order) order=order)
if smooth > 0:
frame = frame.astype(self.dtype)
data_frames += (frame,) data_frames += (frame,)
data = np.stack(data_frames) data = np.stack(data_frames)

View File

@@ -1,6 +1,5 @@
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
from .layer import Layer
# 3rd-party # 3rd-party
import SimpleITK as sitk import SimpleITK as sitk
@@ -166,7 +165,7 @@ def remove_end_str(string: str, end: str):
return string 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) """Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
Args: Args:

View File

@@ -1,5 +1,10 @@
from ast import literal_eval
from pathlib import Path
import bpy 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)): def move_node_to_node(node, target_node, offset=(0, 0)):
node.location.x = target_node.location.x + offset[0] node.location.x = target_node.location.x + offset[0]
@@ -48,20 +53,65 @@ def get_container_obj(current_obj):
return None 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 node_group = layer_obj.modifiers[0].node_group
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0] 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": if type(value).__name__ == "bpy_prop_array":
value = tuple(value) value = tuple(value)
return tuple([int(v) for v in value]) \ return tuple([int(v) for v in value]) \
if prop in ["shape"] else value if prop in ["shape"] else value
elif type(value).__name__ == "str": elif type(value).__name__ == "str":
return str(value) return str(value)
if type(value).__name__ == "float": elif type(value).__name__ == "float":
value = float(value) value = float(value)
return round(value, 2) \ return round(value, 2) \
if prop in ["bioxel_size"] else value 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): 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 return layer_objs
def add_driver(target, target_prop, var_sources, expression): def add_driver(target, target_prop, var_sources, expression):
driver = target.driver_add(target_prop) driver = target.driver_add(target_prop)
is_vector = isinstance(driver, list) is_vector = isinstance(driver, list)
@@ -126,3 +177,69 @@ def add_direct_driver(target, target_prop, source, source_prop):
] ]
expression = "var0" 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 .layer import Layer, layer_to_obj, obj_to_layer
from ..nodes import custom_nodes from .common import (get_container_layer_objs,
from .utils import (get_container_layer_objs,
get_layer_prop_value, get_layer_prop_value,
get_nodes_by_type, get_nodes_by_type,
move_node_to_node) move_node_to_node)
from .node import add_node_to_graph
from ..utils import get_use_link
NODE_TYPE = { NODE_TYPE = {
"label": "BioxelNodes_MaskByLabel", "label": "BioxelNodes_MaskByLabel",
@@ -82,13 +82,17 @@ def add_layers(layers: list[Layer],
cache_dir: str): cache_dir: str):
node_group = container_obj.modifiers[0].node_group node_group = container_obj.modifiers[0].node_group
try:
output_node = get_nodes_by_type(node_group, output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0] 'NodeGroupOutput')[0]
except:
output_node = node_group.nodes.new("NodeGroupOutput")
for i, layer in enumerate(layers): for i, layer in enumerate(layers):
layer_obj = layer_to_obj(layer, container_obj, cache_dir) layer_obj = layer_to_obj(layer, container_obj, cache_dir)
fetch_node = custom_nodes.add_node(node_group, fetch_node = add_node_to_graph("FetchLayer",
"BioxelNodes_FetchLayer") node_group,
get_use_link())
fetch_node.label = get_layer_prop_value(layer_obj, "name") fetch_node.label = get_layer_prop_value(layer_obj, "name")
fetch_node.inputs[0].default_value = layer_obj fetch_node.inputs[0].default_value = layer_obj
@@ -104,6 +108,7 @@ def add_layers(layers: list[Layer],
def container_to_obj(container: Container, def container_to_obj(container: Container,
scene_scale: float, scene_scale: float,
step_size: float,
cache_dir: str): cache_dir: str):
# Wrapper a Container # Wrapper a Container
@@ -134,11 +139,26 @@ def container_to_obj(container: Container,
container_obj.name = container.name container_obj.name = container.name
container_obj.data.name = container.name container_obj.data.name = container.name
container_obj.show_in_front = True 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['bioxel_container'] = True
container_obj["scene_scale"] = scene_scale
container_obj["step_size"] = step_size
modifier = container_obj.modifiers.new("GeometryNodes", 'NODES') modifier = container_obj.modifiers.new("GeometryNodes", 'NODES')
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree') 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", in_out="OUTPUT",
socket_type="NodeSocketGeometry") socket_type="NodeSocketGeometry")
modifier.node_group = node_group modifier.node_group = node_group

View File

@@ -7,10 +7,11 @@ import pyopenvdb as vdb
from pathlib import Path from pathlib import Path
from uuid import uuid4 from uuid import uuid4
from ..nodes import custom_nodes
from ..bioxel.layer import Layer from ..bioxel.layer import Layer
from .utils import (get_layer_prop_value, from ..utils import get_use_link
move_node_between_nodes, add_direct_driver) 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): 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)) grids, base_metadata = vdb.readAll(str(f))
grid = grids[0] grid = grids[0]
metadata = grid.metadata 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) grid.copyToArray(data_frame)
data_frames += (data_frame,) data_frames += (data_frame,)
data = np.stack(data_frames) 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)) grids, base_metadata = vdb.readAll(str(cache_filepath))
grid = grids[0] grid = grids[0]
metadata = grid.metadata 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) grid.copyToArray(data)
data = np.expand_dims(data, axis=0) # expend frame 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 = bpy.data.volumes.new(layer_display_name)
layer_data.render.space = 'WORLD' 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.sequence_mode = 'REPEAT'
layer_data.filepath = str(cache_filepaths[0]) layer_data.filepath = str(cache_filepaths[0])
# add_direct_driver(layer_data, "render.step_size",
# container_obj, "scale[0]")
if layer.frame_count > 1: if layer.frame_count > 1:
layer_data.is_sequence = True layer_data.is_sequence = True
layer_data.frame_duration = layer.frame_count layer_data.frame_duration = layer.frame_count
@@ -164,8 +172,7 @@ def layer_to_obj(layer: Layer,
socket_type="NodeSocketGeometry") socket_type="NodeSocketGeometry")
modifier.node_group = node_group modifier.node_group = node_group
layer_node = custom_nodes.add_node(node_group, layer_node = add_node_to_graph("_Layer", node_group, get_use_link())
"BioxelNodes__Layer")
layer_node.inputs['name'].default_value = layer.name layer_node.inputs['name'].default_value = layer.name
layer_node.inputs['shape'].default_value = layer.shape layer_node.inputs['shape'].default_value = layer.shape
@@ -176,11 +183,12 @@ def layer_to_obj(layer: Layer,
affine_key = f"affine{i}{j}" affine_key = f"affine{i}{j}"
layer_node.inputs[affine_key].default_value = layer.affine[j, i] layer_node.inputs[affine_key].default_value = layer.affine[j, i]
layer_node.inputs['id'].default_value = random.randint(-200000000, layer_node.inputs['unique'].default_value = random.uniform(0, 1)
200000000)
layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0] layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0]
layer_node.inputs['dtype'].default_value = layer.dtype.str layer_node.inputs['dtype'].default_value = layer.dtype.str
layer_node.inputs['dtype_num'].default_value = layer.dtype.num 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['offset'].default_value = max(0, -layer.min)
layer_node.inputs['min'].default_value = layer.min layer_node.inputs['min'].default_value = layer.min
layer_node.inputs['max'].default_value = layer.max 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" schema_version = "1.0.0"
id = "bioxelnodes" id = "bioxelnodes"
version = "0.4.0" version = "1.0.0"
name = "Bioxel Nodes" name = "Bioxel Nodes"
tagline = "For scientific volumetric data visualization in Blender" tagline = "For scientific volumetric data visualization in Blender"
maintainer = "Ma Nan <icrdr2010@outlook.com>" maintainer = "Ma Nan <icrdr2010@outlook.com>"

View File

@@ -1,8 +1,9 @@
from pathlib import Path 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 = [ MENU_ITEMS = [
{ {
@@ -12,34 +13,40 @@ MENU_ITEMS = [
{ {
'label': 'Cutout by Threshold', 'label': 'Cutout by Threshold',
'icon': 'EMPTY_SINGLE_ARROW', 'icon': 'EMPTY_SINGLE_ARROW',
'node_type': 'BioxelNodes_CutoutByThreshold', 'name': 'CutoutByThreshold',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Cutout by Range', 'label': 'Cutout by Range',
'icon': 'IPO_CONSTANT', 'icon': 'IPO_CONSTANT',
'node_type': 'BioxelNodes_CutoutByRange', 'name': 'CutoutByRange',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Cutout by Color', 'label': 'Cutout by Color',
'icon': 'COLOR', 'icon': 'COLOR',
'node_type': 'BioxelNodes_CutoutByColor', 'name': 'CutoutByColor',
'node_description': '' 'description': ''
}, },
"separator", "separator",
{ {
'label': 'To Surface', 'label': 'To Surface',
'icon': 'MESH_DATA', 'icon': 'MESH_DATA',
'node_type': 'BioxelNodes_ToSurface', 'name': 'ToSurface',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Join Component', 'label': 'Join Component',
'icon': 'CONSTRAINT_BONE', 'icon': 'CONSTRAINT_BONE',
'node_type': 'BioxelNodes_JoinComponent', 'name': 'JoinComponent',
'node_description': '' 'description': ''
}, },
{
'label': 'Slice',
'icon': 'TEXTURE',
'name': 'Slice',
'description': ''
}
] ]
}, },
{ {
@@ -49,59 +56,46 @@ MENU_ITEMS = [
{ {
'label': 'Set Properties', 'label': 'Set Properties',
'icon': 'PROPERTIES', 'icon': 'PROPERTIES',
'node_type': 'BioxelNodes_SetProperties', 'name': 'SetProperties',
'node_description': '' 'description': ''
}, },
"separator", "separator",
{ {
'label': 'Set Color', 'label': 'Set Color',
'icon': 'IPO_SINE', 'icon': 'IPO_SINE',
'node_type': 'BioxelNodes_SetColor', 'name': 'SetColor',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Set Color by Color', 'label': 'Set Color by Color',
'icon': 'IPO_QUINT', 'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByColor', 'name': 'SetColorByColor',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Set Color by Ramp 2', 'label': 'Set Color by Ramp 2',
'icon': 'IPO_QUAD', 'icon': 'IPO_QUAD',
'node_type': 'BioxelNodes_SetColorByRamp2', 'name': 'SetColorByRamp2',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Set Color by Ramp 3', 'label': 'Set Color by Ramp 3',
'icon': 'IPO_CUBIC', 'icon': 'IPO_CUBIC',
'node_type': 'BioxelNodes_SetColorByRamp3', 'name': 'SetColorByRamp3',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Set Color by Ramp 4', 'label': 'Set Color by Ramp 4',
'icon': 'IPO_QUART', 'icon': 'IPO_QUART',
'node_type': 'BioxelNodes_SetColorByRamp4', 'name': 'SetColorByRamp4',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Set Color by Ramp 5', 'label': 'Set Color by Ramp 5',
'icon': 'IPO_QUINT', 'icon': 'IPO_QUINT',
'node_type': 'BioxelNodes_SetColorByRamp5', 'name': 'SetColorByRamp5',
'node_description': '' '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': ''
# },
] ]
}, },
{ {
@@ -111,39 +105,45 @@ MENU_ITEMS = [
{ {
'label': 'Membrane Shader', 'label': 'Membrane Shader',
'icon': 'NODE_MATERIAL', 'icon': 'NODE_MATERIAL',
'node_type': 'BioxelNodes_AssignMembraneShader', 'name': 'AssignMembraneShader',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Solid Shader', 'label': 'Solid Shader',
'icon': 'SHADING_SOLID', 'icon': 'SHADING_SOLID',
'node_type': 'BioxelNodes_AssignSolidShader', 'name': 'AssignSolidShader',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Slime Shader', 'label': 'Slime Shader',
'icon': 'OUTLINER_DATA_META', 'icon': 'OUTLINER_DATA_META',
'node_type': 'BioxelNodes_AssignSlimeShader', 'name': 'AssignSlimeShader',
'node_description': '' 'description': ''
}, },
"separator",
{ ]
'label': 'Inflate',
'icon': 'OUTLINER_OB_META',
'node_type': 'BioxelNodes_M_Inflate',
'node_description': ''
}, },
{ {
'label': 'Smooth', 'label': 'Transform',
'icon': 'MOD_SMOOTH', 'icon': 'EMPTY_AXIS',
'node_type': 'BioxelNodes_M_Smooth', 'items': [
'node_description': '' {
'label': 'Transform',
'icon': 'EMPTY_AXIS',
'name': 'Transform',
'description': ''
}, },
{ {
'label': 'Remove Small Island', 'label': 'Transform Parent',
'icon': 'FORCE_LENNARDJONES', 'icon': 'ORIENTATION_PARENT',
'node_type': 'BioxelNodes_M_RemoveSmallIsland', 'name': 'TransformParent',
'node_description': '' 'description': ''
},
{
'label': 'ReCenter',
'icon': 'PROP_CON',
'name': 'ReCenter',
'description': ''
} }
] ]
}, },
@@ -154,21 +154,21 @@ MENU_ITEMS = [
{ {
'label': 'Cut', 'label': 'Cut',
'icon': 'MOD_BEVEL', 'icon': 'MOD_BEVEL',
'node_type': 'BioxelNodes_Cut', 'name': 'Cut',
'node_description': '' 'description': ''
}, },
"separator", "separator",
{ {
'label': 'Primitive Cutter', 'label': 'Primitive Cutter',
'icon': 'MOD_LINEART', 'icon': 'MOD_LINEART',
'node_type': 'BioxelNodes_PrimitiveCutter', 'name': 'PrimitiveCutter',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Object Cutter', 'label': 'Object Cutter',
'icon': 'MESH_PLANE', 'icon': 'MESH_PLANE',
'node_type': 'BioxelNodes_ObjectCutter', 'name': 'ObjectCutter',
'node_description': '' 'description': ''
} }
] ]
}, },
@@ -177,41 +177,48 @@ MENU_ITEMS = [
'icon': 'MODIFIER', 'icon': 'MODIFIER',
'items': [ 'items': [
{ {
'label': 'Pick Mesh', 'label': 'Pick Surface',
'icon': 'OUTLINER_OB_MESH', 'icon': 'OUTLINER_OB_MESH',
'node_type': 'BioxelNodes_PickMesh', 'name': 'PickSurface',
'node_description': '' 'description': ''
}, },
{ {
'label': 'Pick Volume', 'label': 'Pick Volume',
'icon': 'OUTLINER_OB_VOLUME', 'icon': 'OUTLINER_OB_VOLUME',
'node_type': 'BioxelNodes_PickVolume', 'name': 'PickVolume',
'node_description': '' 'description': ''
},
{
'label': 'Pick Shape Wire',
'icon': 'FILE_VOLUME',
'name': 'PickShapeWire',
'description': ''
}, },
{ {
'label': 'Pick Bbox Wire', 'label': 'Pick Bbox Wire',
'icon': 'MESH_CUBE', 'icon': 'MESH_CUBE',
'node_type': 'BioxelNodes_PickBboxWire', 'name': 'PickBboxWire',
'node_description': '' '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): def __init__(self):
message = 'Cancelled by user' message = 'Cancelled by user'
super().__init__(message) 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 import bpy
from .operators.utils import get_layer_item_label from .constants import MENU_ITEMS, NODE_LIB_FILENAME
from .bioxelutils.utils import (get_container_obj, from .node_menu import NodeMenu
get_container_objs_from_selection,
get_container_layer_objs, from .bioxelutils.common import (get_container_obj,
get_container_layer_objs, get_layer_label,
get_layer_prop_value) get_layer_prop_value)
from .operators.layer import (FetchLayerMenu, FetchLayer, from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer,
RemoveLayer, RemoveMissingLayers, RenameLayer, ResampleScalar, SaveLayerCache, RemoveSelectedLayers, SaveSelectedLayersCache,
SignScalar, CombineLabels, ResampleLayer, SignScalar, CombineLabels,
FillByLabel, FillByThreshold, FillByRange, get_selected_objs_in_node_tree) FillByLabel, FillByThreshold, FillByRange)
from .operators.container import (SaveAllLayerCaches, SaveContainer, LoadContainer, from .operators.container import (AddLocator, AddSlicer, ExtractShapeWire,
SaveContainerLayersCache, RemoveContainerMissingLayers,
SaveContainer, LoadContainer,
AddPieCutter, AddPlaneCutter, AddPieCutter, AddPlaneCutter,
AddCylinderCutter, AddCubeCutter, AddSphereCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter,
PickBboxWire, PickMesh, PickVolume, ScaleContainer) ExtractBboxWire, ExtractSurface, ExtractVolume, ContainerProps)
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor) from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
from .operators.misc import (CleanAllCaches, from .operators.misc import (CleanTemp,
ReLinkNodes, RenderSettingPreset, SaveStagedData, SliceViewer) 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_idname = "BIOXELNODES_MT_PICK"
bl_label = "Pick from Container" bl_label = "Extract from Container"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(PickMesh.bl_idname) layout.operator(ExtractSurface.bl_idname)
layout.operator(PickVolume.bl_idname) layout.operator(ExtractVolume.bl_idname)
layout.operator(PickBboxWire.bl_idname) layout.operator(ExtractShapeWire.bl_idname)
layout.operator(ExtractBboxWire.bl_idname)
class AddCutterMenu(bpy.types.Menu): class AddCutterMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_CUTTERS" bl_idname = "BIOXELNODES_MT_CUTTERS"
bl_label = "Add a Object Cutter" bl_label = "Add a Cutter"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator(AddPlaneCutter.bl_idname, text="Plane Cutter") layout.operator(AddPlaneCutter.bl_idname,
layout.operator(AddCylinderCutter.bl_idname, text="Cylinder Cutter") icon=AddPlaneCutter.bl_icon, text="Plane Cutter")
layout.operator(AddCubeCutter.bl_idname, text="Cube Cutter") layout.operator(AddCylinderCutter.bl_idname,
layout.operator(AddSphereCutter.bl_idname, text="Sphere Cutter") icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter")
layout.operator(AddPieCutter.bl_idname, text="Pie 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): class ImportLayerMenu(bpy.types.Menu):
@@ -60,7 +84,7 @@ class ImportLayerMenu(bpy.types.Menu):
class AddLayerMenu(bpy.types.Menu): class AddLayerMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_ADDLAYER" bl_idname = "BIOXELNODES_MT_ADDLAYER"
bl_label = "Import Volumetric Data (Add to)" bl_label = "Import Volumetric Data (Add)"
bl_icon = "FILE_NEW" bl_icon = "FILE_NEW"
def draw(self, context): def draw(self, context):
@@ -79,31 +103,37 @@ class ModifyLayerMenu(bpy.types.Menu):
bl_icon = "FILE_NEW" bl_icon = "FILE_NEW"
def draw(self, context): 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 = self.layout
op = layout.operator(ResampleScalar.bl_idname, layout.operator(SignScalar.bl_idname,
icon=ResampleScalar.bl_icon)
op.layer_obj_name = active_obj_name
op = layout.operator(SignScalar.bl_idname,
icon=SignScalar.bl_icon) icon=SignScalar.bl_icon)
op.layer_obj_name = active_obj_name layout.operator(FillByThreshold.bl_idname,
op = layout.operator(FillByThreshold.bl_idname,
icon=FillByThreshold.bl_icon) icon=FillByThreshold.bl_icon)
op.layer_obj_name = active_obj_name layout.operator(FillByRange.bl_idname,
op = layout.operator(FillByRange.bl_idname,
icon=FillByRange.bl_icon) icon=FillByRange.bl_icon)
op.layer_obj_name = active_obj_name layout.operator(FillByLabel.bl_idname,
op = layout.operator(FillByLabel.bl_idname,
icon=FillByLabel.bl_icon) icon=FillByLabel.bl_icon)
op.layer_obj_name = active_obj_name layout.operator(CombineLabels.bl_idname,
op = layout.operator(CombineLabels.bl_idname,
icon=CombineLabels.bl_icon) icon=CombineLabels.bl_icon)
op.layer_obj_name = active_obj_name
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): class RenderSettingMenu(bpy.types.Menu):
@@ -118,6 +148,15 @@ class RenderSettingMenu(bpy.types.Menu):
op.preset = k 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): class BioxelNodesTopbarMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_TOPBAR" bl_idname = "BIOXELNODES_MT_TOPBAR"
bl_label = "Bioxel Nodes" bl_label = "Bioxel Nodes"
@@ -130,9 +169,15 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
layout.operator(LoadContainer.bl_idname) layout.operator(LoadContainer.bl_idname)
layout.separator() layout.separator()
layout.operator(SaveStagedData.bl_idname) layout.operator(SaveAllLayersCache.bl_idname)
layout.operator(ReLinkNodes.bl_idname) layout.operator(RemoveAllMissingLayers.bl_idname)
layout.operator(CleanAllCaches.bl_idname)
layout.separator()
layout.operator(SaveNodeLib.bl_idname)
layout.menu(ReLinkNodeLibMenu.bl_idname)
layout.separator()
layout.menu(DangerZoneMenu.bl_idname)
layout.separator() layout.separator()
layout.menu(RenderSettingMenu.bl_idname) layout.menu(RenderSettingMenu.bl_idname)
@@ -143,51 +188,66 @@ def TOPBAR(self, context):
layout.menu(BioxelNodesTopbarMenu.bl_idname) layout.menu(BioxelNodesTopbarMenu.bl_idname)
class BioxelNodesNodeMenu(bpy.types.Menu): class NodeHeadMenu(bpy.types.Menu):
bl_idname = "BIOXELNODES_MT_NODE" bl_idname = "BIOXELNODES_MT_NODE_HEAD"
bl_label = "Bioxel Nodes" bl_label = "Bioxel Nodes"
bl_icon = "FILE_VOLUME" bl_icon = "FILE_VOLUME"
def draw(self, context): 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 = self.layout
layout.separator()
layout.menu(AddLayerMenu.bl_idname, layout.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon) icon=AddLayerMenu.bl_icon)
layout.separator()
layout.operator(SaveContainer.bl_idname) layout.operator(SaveContainer.bl_idname)
layout.separator() 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(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.separator()
layout.menu(FetchLayerMenu.bl_idname) 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) class NodeContextMenu(bpy.types.Menu):
op.layer_obj_name = active_obj_name 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.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 container_obj = context.object
is_geo_nodes = context.area.ui_type == "GeometryNodeTree" is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
is_container = get_container_obj(container_obj) is_container = get_container_obj(container_obj)
@@ -197,10 +257,23 @@ def NODE(self, context):
layout = self.layout layout = self.layout
layout.separator() 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 container_obj = context.object
is_geo_nodes = context.area.ui_type == "GeometryNodeTree" is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
is_container = get_container_obj(container_obj) 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_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
layer_list = layer_list_UL.layer_list layer_list = layer_list_UL.layer_list
layer_list_active = layer_list_UL.layer_list_active
layer_list.clear() layer_list.clear()
for layer_obj in get_container_layer_objs(container_obj): for layer_obj in get_container_layer_objs(container_obj):
layer_item = layer_list.add() 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.obj_name = layer_obj.name
layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}" layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
for prop in ["kind", "bioxel_size", "shape", "min", "max", ]]) for prop in ["kind",
"bioxel_size",
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list): "shape",
active_obj_name = layer_list[layer_list_active].obj_name "frame_count",
else: "channel_count",
active_obj_name = "" "min", "max"]])
layout = self.layout layout = self.layout
layout.label(text="Layer List") layout.label(text="Layer List")
@@ -243,48 +315,41 @@ def NODE_PT(self, context):
item_dyntip_propname="info_text", item_dyntip_propname="info_text",
rows=20) rows=20)
sidebar = split.column(align=True) # sidebar = split.column(align=True)
sidebar.menu(AddLayerMenu.bl_idname, # sidebar.menu(AddLayerMenu.bl_idname,
icon=AddLayerMenu.bl_icon, text="") # icon=AddLayerMenu.bl_icon, text="")
sidebar.operator(SaveAllLayerCaches.bl_idname, # sidebar.operator(SaveContainerLayersCache.bl_idname,
icon=SaveAllLayerCaches.bl_icon, text="") # icon=SaveContainerLayersCache.bl_icon, text="")
sidebar.operator(RemoveMissingLayers.bl_idname, # sidebar.operator(RemoveContainerMissingLayers.bl_idname,
icon=RemoveMissingLayers.bl_icon, text="") # icon=RemoveContainerMissingLayers.bl_icon, text="")
sidebar.separator() # sidebar.separator()
op = sidebar.operator(FetchLayer.bl_idname, # sidebar.operator(SaveSelectedLayersCache.bl_idname,
icon=FetchLayer.bl_icon, text="") # icon=SaveSelectedLayersCache.bl_icon, text="")
op.layer_obj_name = active_obj_name # sidebar.operator(RemoveSelectedLayers.bl_idname,
op = sidebar.operator(SaveLayerCache.bl_idname, # icon=RemoveSelectedLayers.bl_icon, text="")
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.separator()
op = sidebar.operator(ResampleScalar.bl_idname, # sidebar.operator(RenameLayer.bl_idname,
icon=ResampleScalar.bl_icon, text="") # icon=RenameLayer.bl_icon, text="")
op.layer_obj_name = active_obj_name # sidebar.operator(ResampleLayer.bl_idname,
op = sidebar.operator(SignScalar.bl_idname, # icon=ResampleLayer.bl_icon, text="")
icon=SignScalar.bl_icon, text="") # sidebar.operator(RetimeLayer.bl_idname,
op.layer_obj_name = active_obj_name # icon=RetimeLayer.bl_icon, text="")
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.separator()
layout.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): def VIEW3D_TOPBAR(self, context):
@@ -293,15 +358,24 @@ def VIEW3D_TOPBAR(self, context):
icon=SliceViewer.bl_icon, text="") icon=SliceViewer.bl_icon, text="")
node_menu = NodeMenu(
menu_items=MENU_ITEMS
)
def add(): def add():
node_menu.register()
bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR) 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.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(): def remove():
bpy.types.VIEW3D_HT_header.remove(VIEW3D_TOPBAR) 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.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 import bmesh
from ..nodes import custom_nodes from ..utils import get_cache_dir, get_use_link, select_object
from ..customnodes.nodes import AddCustomNode
from ..bioxel.io import load_container, save_container from ..bioxel.io import load_container, save_container
from ..bioxelutils.container import container_to_obj, obj_to_container from ..bioxelutils.container import container_to_obj, obj_to_container
from ..bioxelutils.utils import (get_container_layer_objs, get_container_obj, from ..bioxelutils.node import add_node_to_graph
get_layer_prop_value, get_nodes_by_type, from ..bioxelutils.common import (get_container_layer_objs, get_container_obj,
get_nodes_by_type, is_missing_layer,
move_node_between_nodes, move_node_between_nodes,
move_node_to_node, move_node_to_node,
get_all_layer_objs) get_all_layer_objs)
from .layer import RemoveLayers, SaveLayersCache
from .utils import get_cache_dir, select_object
class SaveContainer(bpy.types.Operator): class SaveContainer(bpy.types.Operator):
@@ -63,7 +62,7 @@ class LoadContainer(bpy.types.Operator):
container_obj = container_to_obj(container, container_obj = container_to_obj(container,
scene_scale=0.01, scene_scale=0.01,
cache_dir=get_cache_dir(context)) cache_dir=get_cache_dir())
select_object(container_obj) select_object(container_obj)
if is_first_import: if is_first_import:
@@ -78,7 +77,7 @@ class LoadContainer(bpy.types.Operator):
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
class PickObject(): class ExtractObject():
bl_options = {'UNDO'} bl_options = {'UNDO'}
def execute(self, context): 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)) size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
obj = bpy.context.active_object 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() bpy.ops.node.new_geometry_nodes_modifier()
modifier = obj.modifiers[0] modifier = obj.modifiers[0]
node_group = modifier.node_group node_group = modifier.node_group
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0] output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
pick_mesh_node = custom_nodes.add_node(node_group, fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
f"BioxelNodes_Pick{self.object_type}") node_group,
get_use_link())
pick_mesh_node.inputs[0].default_value = container_obj fetch_mesh_node.inputs[0].default_value = container_obj
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0]) node_group.links.new(fetch_mesh_node.outputs[0], output_node.inputs[0])
select_object(obj) select_object(obj)
@@ -112,26 +112,34 @@ class PickObject():
return {'FINISHED'} return {'FINISHED'}
class PickMesh(bpy.types.Operator, PickObject): class ExtractSurface(bpy.types.Operator, ExtractObject):
bl_idname = "bioxelnodes.pick_mesh" bl_idname = "bioxelnodes.fetch_mesh"
bl_label = "Pick Mesh" bl_label = "Extract Surface"
bl_description = "Pick Container Mesh" bl_description = "Extract Surface"
bl_icon = "OUTLINER_OB_MESH" 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_idname = "bioxelnodes.pick_volume"
bl_label = "Pick Volume" bl_label = "Extract Volume"
bl_description = "Pick Container Volume" bl_description = "Extract Volume"
bl_icon = "OUTLINER_OB_VOLUME" bl_icon = "OUTLINER_OB_VOLUME"
object_type = "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_idname = "bioxelnodes.pick_bbox_wire"
bl_label = "Pick Bbox Wire" bl_label = "Extract Bbox Wire"
bl_description = "Pick Container Bbox Wire" bl_description = "Extract Bbox Wire"
bl_icon = "MESH_CUBE" bl_icon = "MESH_CUBE"
object_type = "BboxWire" object_type = "BboxWire"
@@ -162,14 +170,12 @@ class AddCutter():
elif self.cutter_type == "pie": elif self.cutter_type == "pie":
bpy.ops.mesh.primitive_plane_add( bpy.ops.mesh.primitive_plane_add(
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
pie = bpy.context.active_object pie_obj = bpy.context.active_object
pie.name = "Pie" orig_data = pie_obj.data
orig_data = pie.data
# Create mesh # Create mesh
pie_mesh = bpy.data.meshes.new('Pie') pie_mesh = bpy.data.meshes.new('Pie')
pie_obj.data = pie_mesh
pie.data = pie_mesh
bpy.data.meshes.remove(orig_data) bpy.data.meshes.remove(orig_data)
# # Create object # # Create object
@@ -204,7 +210,19 @@ class AddCutter():
# bpy.context.view_layer.objects.active = pie # bpy.context.view_layer.objects.active = pie
cutter_obj = bpy.context.active_object 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_camera = False
cutter_obj.visible_diffuse = False cutter_obj.visible_diffuse = False
cutter_obj.visible_glossy = False cutter_obj.visible_glossy = False
@@ -213,6 +231,7 @@ class AddCutter():
cutter_obj.visible_shadow = False cutter_obj.visible_shadow = False
cutter_obj.hide_render = True cutter_obj.hide_render = True
cutter_obj.display_type = 'WIRE' cutter_obj.display_type = 'WIRE'
cutter_obj.lineart.usage = 'EXCLUDE'
select_object(container_obj) select_object(container_obj)
@@ -222,13 +241,16 @@ class AddCutter():
cut_nodes = get_nodes_by_type(node_group, cut_nodes = get_nodes_by_type(node_group,
'BioxelNodes_Cut') 'BioxelNodes_Cut')
if len(cut_nodes) == 0: if len(cut_nodes) == 0:
cutter_node = custom_nodes.add_node(node_group, cutter_node = add_node_to_graph("ObjectCutter",
"BioxelNodes_ObjectCutter") node_group,
get_use_link())
cutter_node.inputs[0].default_value = self.cutter_type.capitalize() cutter_node.inputs[0].default_value = self.cutter_type.capitalize()
cutter_node.inputs[1].default_value = cutter_obj cutter_node.inputs[1].default_value = cutter_obj
cut_node = custom_nodes.add_node(node_group, cut_node = add_node_to_graph("Cut",
'BioxelNodes_Cut') node_group,
get_use_link())
output_node = get_nodes_by_type(node_group, output_node = get_nodes_by_type(node_group,
'NodeGroupOutput')[0] 'NodeGroupOutput')[0]
if len(output_node.inputs[0].links) == 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)) move_node_to_node(cutter_node, cut_node, (-300, -300))
else: else:
bpy.ops.bioxelnodes.object_cutter('INVOKE_DEFAULT', bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
cutter_obj_name=cutter_obj.name, node_name="ObjectCutter",
cutter_type=self.cutter_type.capitalize()) node_label=name)
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 node = bpy.context.active_node
node.inputs[0].default_value = name
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 node.inputs[1].default_value = cutter_obj
return {"FINISHED"} 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): class AddPlaneCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_plane_cutter" bl_idname = "bioxelnodes.add_plane_cutter"
bl_label = "Add a 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" bl_icon = "MESH_PLANE"
cutter_type = "plane" cutter_type = "plane"
@@ -308,7 +292,7 @@ class AddPlaneCutter(bpy.types.Operator, AddCutter):
class AddCylinderCutter(bpy.types.Operator, AddCutter): class AddCylinderCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cylinder_cutter" bl_idname = "bioxelnodes.add_cylinder_cutter"
bl_label = "Add a 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" bl_icon = "MESH_CYLINDER"
cutter_type = "cylinder" cutter_type = "cylinder"
@@ -316,7 +300,7 @@ class AddCylinderCutter(bpy.types.Operator, AddCutter):
class AddCubeCutter(bpy.types.Operator, AddCutter): class AddCubeCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_cube_cutter" bl_idname = "bioxelnodes.add_cube_cutter"
bl_label = "Add a 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" bl_icon = "MESH_CUBE"
cutter_type = "cube" cutter_type = "cube"
@@ -324,7 +308,7 @@ class AddCubeCutter(bpy.types.Operator, AddCutter):
class AddSphereCutter(bpy.types.Operator, AddCutter): class AddSphereCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_sphere_cutter" bl_idname = "bioxelnodes.add_sphere_cutter"
bl_label = "Add a 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" bl_icon = "MESH_UVSPHERE"
cutter_type = "sphere" cutter_type = "sphere"
@@ -332,15 +316,128 @@ class AddSphereCutter(bpy.types.Operator, AddCutter):
class AddPieCutter(bpy.types.Operator, AddCutter): class AddPieCutter(bpy.types.Operator, AddCutter):
bl_idname = "bioxelnodes.add_pie_cutter" bl_idname = "bioxelnodes.add_pie_cutter"
bl_label = "Add a 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" bl_icon = "MESH_CONE"
cutter_type = "pie" cutter_type = "pie"
class ScaleContainer(bpy.types.Operator): class AddSlicer(bpy.types.Operator):
bl_idname = "bioxelnodes.scale_container" bl_idname = "bioxelnodes.add_slicer"
bl_label = "Scale Container" bl_label = "Add a Slicer"
bl_description = "Scale Container." 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" bl_icon = "FILE_TICK"
scene_scale: bpy.props.FloatProperty(name="Scene Scale", scene_scale: bpy.props.FloatProperty(name="Scene Scale",
@@ -348,6 +445,11 @@ class ScaleContainer(bpy.types.Operator):
min=1e-6, max=1e6, min=1e-6, max=1e6,
default=0.01) # type: ignore 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): def execute(self, context):
container_obj = get_container_obj(context.object) 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[0] = self.scene_scale
container_obj.scale[1] = self.scene_scale container_obj.scale[1] = self.scene_scale
container_obj.scale[2] = 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): for layer_obj in get_container_layer_objs(container_obj):
bioxel_size = get_layer_prop_value(layer_obj, "bioxel_size") layer_obj.data.render.space = 'WORLD'
layer_obj.data.render.step_size = self.scene_scale * bioxel_size layer_obj.data.render.step_size = self.scene_scale * self.step_size
return {'FINISHED'} return {'FINISHED'}
@@ -371,49 +475,51 @@ class ScaleContainer(bpy.types.Operator):
if container_obj is None: if container_obj is None:
return self.execute(context) return self.execute(context)
else: else:
self.scene_scale = container_obj.scale[0] self.scene_scale = container_obj.get("scene_scale") or 0.01
context.window_manager.invoke_props_dialog(self, self.step_size = container_obj.get("step_size") or 1
width=500) context.window_manager.invoke_props_dialog(self)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
class SaveAllLayerCaches(bpy.types.Operator): def get_container_layers(context, layer_filter=None):
bl_idname = "bioxelnodes.save_all_layer_caches" def _layer_filter(layer_obj, context):
bl_label = "Save All Layer Caches" return True
bl_description = "Save Container's caches to directory."
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" bl_icon = "FILE_TICK"
cache_dir: bpy.props.StringProperty( success_msg = "Successfully saved all container layers."
name="Cache Directory",
subtype='DIR_PATH',
default="//"
) # type: ignore
def execute(self, context): def get_layers(self, context):
container_obj = get_container_obj(context.object) 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 = [] class RemoveContainerMissingLayers(bpy.types.Operator, RemoveLayers):
for layer_obj in get_container_layer_objs(container_obj): bl_idname = "bioxelnodes.remove_container_missing_layers"
try: bl_label = "Remove Container Missing Layers"
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT', bl_description = "Remove all current container missing layers"
layer_obj_name=layer_obj.name, bl_icon = "BRUSH_DATA"
cache_dir=self.cache_dir)
except:
fails.append(layer_obj)
if len(fails) == 0: success_msg = "Successfully removed all container missing layers."
self.report({"INFO"}, f"Successfully saved bioxel layers.")
else:
self.report(
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
return {'FINISHED'} def 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): def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self, context.window_manager.invoke_confirm(self,
width=500) event,
message=f"Are you sure to remove all **Missing** layers?")
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}

View File

@@ -7,9 +7,8 @@ from pathlib import Path
from ..exceptions import CancelledByUser from ..exceptions import CancelledByUser
from ..props import BIOXELNODES_Series from ..props import BIOXELNODES_Series
from ..bioxelutils.utils import (get_all_layer_objs, get_container_obj, from ..bioxelutils.common import (get_all_layer_objs, get_container_obj,
get_layer_obj, get_layer_obj, is_incompatible)
get_container_objs_from_selection)
from ..bioxelutils.container import (Container, from ..bioxelutils.container import (Container,
add_layers, add_layers,
container_to_obj) container_to_obj)
@@ -17,8 +16,9 @@ from ..bioxel.layer import Layer
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS, from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
get_ext, parse_volumetric_data) get_ext, parse_volumetric_data)
from .utils import (get_cache_dir, from ..utils import (get_cache_dir,
progress_update, progress_bar, select_object) progress_update, progress_bar,
select_object)
# 3rd-party # 3rd-party
import SimpleITK as sitk import SimpleITK as sitk
@@ -57,7 +57,7 @@ class ImportVolumetricData():
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
read_as = "scalar" read_as = "SCALAR"
def execute(self, context): def execute(self, context):
data_path = Path(self.filepath).resolve() data_path = Path(self.filepath).resolve()
@@ -83,7 +83,7 @@ class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Scalar" bl_label = "Import as Scalar"
bl_description = "Import Volumetric Data to Container as Scalar" bl_description = "Import Volumetric Data to Container as Scalar"
bl_icon = "EVENT_S" bl_icon = "EVENT_S"
read_as = "scalar" read_as = "SCALAR"
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData): class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
@@ -91,7 +91,7 @@ class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Label" bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label" bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_L" bl_icon = "EVENT_L"
read_as = "label" read_as = "LABEL"
class ImportAsColor(bpy.types.Operator, ImportVolumetricData): class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
@@ -99,7 +99,7 @@ class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
bl_label = "Import as Label" bl_label = "Import as Label"
bl_description = "Import Volumetric Data to Container as Label" bl_description = "Import Volumetric Data to Container as Label"
bl_icon = "EVENT_C" bl_icon = "EVENT_C"
read_as = "color" read_as = "COLOR"
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler): class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
@@ -142,6 +142,8 @@ class ParseVolumetricData(bpy.types.Operator):
bl_options = {'UNDO'} bl_options = {'UNDO'}
meta = None meta = None
label_count = 0
dtype = None
thread = None thread = None
_timer = None _timer = None
@@ -160,10 +162,10 @@ class ParseVolumetricData(bpy.types.Operator):
options={"SKIP_SAVE"}) # type: ignore options={"SKIP_SAVE"}) # type: ignore
read_as: bpy.props.EnumProperty(name="Read as", read_as: bpy.props.EnumProperty(name="Read as",
default="scalar", default="SCALAR",
items=[("scalar", "Scalar", ""), items=[("SCALAR", "Scalar", ""),
("label", "Labels", ""), ("LABEL", "Labels", ""),
("color", "Color", "")]) # type: ignore ("COLOR", "Color", "")]) # type: ignore
series_id: bpy.props.EnumProperty(name="Select Series", series_id: bpy.props.EnumProperty(name="Select Series",
items=get_series_ids) # type: ignore items=get_series_ids) # type: ignore
@@ -172,6 +174,11 @@ class ParseVolumetricData(bpy.types.Operator):
type=BIOXELNODES_Series) # type: ignore type=BIOXELNODES_Series) # type: ignore
def execute(self, context): 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: if not self.filepath:
self.report({"WARNING"}, "No file selected.") self.report({"WARNING"}, "No file selected.")
return {'CANCELLED'} return {'CANCELLED'}
@@ -205,6 +212,8 @@ class ParseVolumetricData(bpy.types.Operator):
return return
self.meta = meta self.meta = meta
self.label_count = int(np.max(data))
self.dtype = data.dtype
# Init cancel flag # Init cancel flag
self.is_cancelled = False self.is_cancelled = False
@@ -270,25 +279,48 @@ class ParseVolumetricData(bpy.types.Operator):
for key, value in self.meta.items(): for key, value in self.meta.items():
print(f"{key}: {value}") 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_shape = self.meta['xyz_shape']
orig_spacing = self.meta['spacing'] 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))) min_log10 = math.floor(math.log10(min(*orig_spacing)))
orig_spacing = (orig_spacing[0] * math.pow(10, -spacing_log10-1), max_log10 = math.floor(math.log10(max(*orig_spacing)))
orig_spacing[1] * math.pow(10, -spacing_log10-1), # 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) 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], bioxel_size = max(min(*orig_spacing), 1.0)
orig_spacing[1], orig_spacing[2])
bioxel_size = max(min_size, 1.0)
# layer_shape = get_layer_shape(1, orig_shape, orig_spacing) layer_shape = get_layer_shape(bioxel_size,
# layer_size = get_layer_size(layer_shape, orig_shape,
# bioxel_size) orig_spacing)
# log10 = math.floor(math.log10(max(*layer_size))) layer_size = get_layer_size(layer_shape,
# log10 = max(1, log10) bioxel_size,
# log10 = min(3, log10) 0.01)
# scene_scale = math.pow(10, -log10) 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 scene_scale = 0.01
if context.area.type == "NODE_EDITOR": if context.area.type == "NODE_EDITOR":
@@ -313,6 +345,7 @@ class ParseVolumetricData(bpy.types.Operator):
channel_count=self.meta['channel_count'], channel_count=self.meta['channel_count'],
container_obj_name=container_obj_name, container_obj_name=container_obj_name,
read_as=self.read_as, read_as=self.read_as,
label_count=self.label_count,
scene_scale=scene_scale scene_scale=scene_scale
) )
@@ -320,6 +353,8 @@ class ParseVolumetricData(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): 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: if context.window_manager.bioxelnodes_progress_factor < 1:
print("A process is executing, please wait for it to finish.") print("A process is executing, please wait for it to finish.")
return {'CANCELLED'} return {'CANCELLED'}
@@ -420,17 +455,17 @@ class ParseVolumetricData(bpy.types.Operator):
layout.prop(self, "series_id") layout.prop(self, "series_id")
def get_sequence_sources(self, context): def get_frame_sources(self, context):
items = [("-1", "None (1 frame)", "")] items = [("-1", "First Frame (1 frame)", "")]
orig_shape = tuple(self.orig_shape) orig_shape = tuple(self.orig_shape)
if self.frame_count > 1: 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: 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: elif self.frame_count == 1 and self.channel_count == 1:
items.append(("1", f"X ({orig_shape[0]} frames)", "")) items.append(("1", f"X-axis ({orig_shape[0]} frames)", ""))
items.append(("2", f"Y ({orig_shape[1]} frames)", "")) items.append(("2", f"Y-axis ({orig_shape[1]} frames)", ""))
items.append(("3", f"Z ({orig_shape[2]} frames)", "")) items.append(("3", f"Z-axis ({orig_shape[2]} frames)", ""))
return items return items
@@ -460,11 +495,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
channel_count: bpy.props.IntProperty() # type: ignore channel_count: bpy.props.IntProperty() # type: ignore
label_count: bpy.props.IntProperty() # type: ignore
read_as: bpy.props.EnumProperty(name="Read as", read_as: bpy.props.EnumProperty(name="Read as",
default="scalar", default="SCALAR",
items=[("scalar", "Scalar", ""), items=[("SCALAR", "Scalar", ""),
("label", "Labels", ""), ("LABEL", "Labels", ""),
("color", "Color", "")]) # type: ignore ("COLOR", "Color", "")]) # type: ignore
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)", bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
soft_min=0.1, soft_max=10.0, soft_min=0.1, soft_max=10.0,
@@ -482,11 +519,14 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
min=1e-6, max=1e6, min=1e-6, max=1e6,
default=0.01) # type: ignore default=0.01) # type: ignore
split_channels: bpy.props.BoolProperty(name="Split Channels", remap: bpy.props.BoolProperty(name="Remap to 0~1",
default=False) # type: ignore default=False) # type: ignore
sequence_source: bpy.props.EnumProperty(name="Time Sequence From", split_channel: bpy.props.BoolProperty(name="Split Channels",
items=get_sequence_sources) # type: ignore default=False) # type: ignore
frame_source: bpy.props.EnumProperty(name="Frame From",
items=get_frame_sources) # type: ignore
def execute(self, context): def execute(self, context):
def import_volumetric_data_func(self, context, cancel): 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) mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size)
affine = np.dot(meta['affine'], mat_scale) affine = np.dot(meta['affine'], mat_scale)
kind = self.read_as kind = self.read_as.lower()
if cancel(): if cancel():
return return
# change shape as sequence or not # change shape as sequence or not
if self.sequence_source == "-1": if self.frame_source == "-1":
data = data[0:1, :, :, :, :] data = data[0:1, :, :, :, :]
elif self.sequence_source == "0": elif self.frame_source == "0":
# frame as frame # frame as frame
pass pass
elif self.sequence_source == "1": elif self.frame_source == "1":
# X as frame # X as frame
data = data.transpose(1, 0, 2, 3, 4) data = data.transpose(1, 0, 2, 3, 4)
shape = (1, shape[1], shape[2]) shape = (1, shape[1], shape[2])
elif self.sequence_source == "2": elif self.frame_source == "2":
# Y as frame # Y as frame
data = data.transpose(2, 1, 0, 3, 4) data = data.transpose(2, 1, 0, 3, 4)
shape = (shape[0], 1, shape[2]) shape = (shape[0], 1, shape[2])
elif self.sequence_source == "3": elif self.frame_source == "3":
# Z as frame # Z as frame
data = data.transpose(3, 1, 2, 0, 4) data = data.transpose(3, 1, 2, 0, 4)
shape = (shape[0], shape[1], 1) shape = (shape[0], shape[1], 1)
@@ -551,6 +591,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
sub_progress = progress + frame * sub_progress_step sub_progress = progress + frame * sub_progress_step
progress_update(context, sub_progress, progress_update(context, sub_progress,
f"Processing {layer_name} Frame {frame+1}...") f"Processing {layer_name} Frame {frame+1}...")
print(f"Processing {layer_name} Frame {frame+1}...")
return progress_callback return progress_callback
layers = [] layers = []
@@ -590,11 +631,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
return return
if kind == "color": if kind == "color":
if np.issubdtype(np.uint8, data.dtype): if np.issubdtype(np.uint8, data.dtype):
data = np.multiply(data, 1.0 / 256, data = np.multiply(data, 1.0 / 256,
dtype=np.float32) dtype=np.float32)
elif data.dtype.kind in ['u', 'i']: elif data.dtype.kind in ['u', 'i']:
# Convert the normalized array to float dtype
data = data.astype(np.float32)
min_val = data.min() min_val = data.min()
max_val = data.max() max_val = data.max()
# Avoid division by zero if all values are the same # 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 # If all values are the same, the normalized array will be all zeros
data = np.zeros_like(data, dtype=np.float32) data = np.zeros_like(data, dtype=np.float32)
# Convert the normalized array to float dtype
data = data.astype(np.float32)
else: else:
data = data.astype(np.float32) data = data.astype(np.float32)
@@ -650,7 +691,21 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
elif kind == "scalar": elif kind == "scalar":
name = self.layer_name or "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 progress_step = 0.7/self.channel_count
for i in range(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 = add_layers(self.layers,
container_obj=container_obj, container_obj=container_obj,
cache_dir=get_cache_dir(context)) cache_dir=get_cache_dir())
else: else:
name = self.container_name or "Container" name = self.container_name or "Container"
container = Container(name=name, container = Container(name=name,
layers=self.layers) layers=self.layers)
step_size = container.layers[0].bioxel_size[0]*5
container_obj = container_to_obj(container, container_obj = container_to_obj(container,
scene_scale=self.scene_scale, scene_scale=self.scene_scale,
cache_dir=get_cache_dir(context)) step_size=step_size,
cache_dir=get_cache_dir())
select_object(container_obj) select_object(container_obj)
@@ -806,55 +863,70 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
# change shape as sequence or not # change shape as sequence or not
channel_count = self.channel_count channel_count = self.channel_count
if self.sequence_source == "-1": frame_count = self.frame_count
if self.frame_source == "-1":
frame_count = 1
pass pass
elif self.sequence_source == "0": elif self.frame_source == "0":
# frame as frame # frame as frame
pass 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]) 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]) 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) layer_shape = (layer_shape[0], layer_shape[1], 1)
else: else:
channel_count = 1 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] 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: if bioxel_count > 100000000:
layer_shape_text += "**TOO LARGE!**" layer_shape_text += "**TOO LARGE!**"
layout = self.layout
if self.container_obj_name == "":
layout.prop(self, "container_name")
layout.prop(self, "layer_name")
panel = layout.box()
panel.prop(self, "bioxel_size")
row = panel.row()
row.prop(self, "orig_spacing")
panel.label(text=layer_shape_text)
panel = layout.box()
panel.prop(self, "sequence_source")
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, layer_size = get_layer_size(layer_shape,
self.bioxel_size, self.bioxel_size,
self.scene_scale) self.scene_scale)
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m" 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 == "":
panel = layout.box() panel = layout.box()
panel.prop(self, "container_name")
panel.prop(self, "scene_scale") panel.prop(self, "scene_scale")
panel.label(text=layer_size_text) 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.prop(self, "frame_source")
if self.read_as == "SCALAR":
panel.prop(self, "split_channel",
text=f"Split Channel as Multi Layer")
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): class ExportVolumetricData(bpy.types.Operator):
bl_idname = "bioxelnodes.export_volumetric_data" bl_idname = "bioxelnodes.export_volumetric_data"

View File

@@ -1,19 +1,16 @@
from pathlib import Path from pathlib import Path
import bpy import bpy
import re
import numpy as np import numpy as np
from ..exceptions import NoContent
from ..bioxel.layer import Layer from ..bioxel.layer import Layer
from ..utils import copy_to_dir from ..bioxelutils.common import (get_container_obj, get_layer_kind, get_layer_label, get_layer_name,
from ..customnodes.nodes import AddCustomNode
from ..bioxelutils.utils import (get_container_obj,
get_layer_prop_value, get_layer_prop_value,
get_container_layer_objs, get_container_layer_objs,
get_node_type, set_layer_prop_value) get_node_type, is_missing_layer, set_layer_prop_value)
from ..bioxelutils.layer import layer_to_obj, obj_to_layer 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 ..utils import get_cache_dir, copy_to_dir
from ..nodes import custom_nodes
def get_label_layer_selection(self, context): 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) container_obj = get_container_obj(bpy.context.active_object)
for layer_obj in get_container_layer_objs(container_obj): 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, items.append((layer_obj.name,
layer_obj.name, name,
"")) ""))
return items return items
def get_selected_objs_in_node_tree(context): class FetchLayer(bpy.types.Operator):
select_objs = [] bl_idname = "bioxelnodes.fetch_layer"
# node_group = context.space_data.edit_tree bl_label = "Fetch Layer"
for node in context.selected_nodes: bl_description = "Fetch layer from current container"
if get_node_type(node) == "BioxelNodes_FetchLayer": bl_icon = "NODE"
layer_obj = node.inputs[0].default_value
if layer_obj != None:
cache_filepath = Path(bpy.path.abspath(
layer_obj.data.filepath)).resolve()
if cache_filepath.is_file():
select_objs.append(layer_obj)
return select_objs
class LayerOperator():
bl_options = {'UNDO'} bl_options = {'UNDO'}
layer_obj_name: bpy.props.StringProperty( layer_obj_name: bpy.props.StringProperty(
options={"HIDDEN"}) # type: ignore 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 @property
def layer_obj(self): def layer_obj(self):
return bpy.data.objects.get(self.layer_obj_name) 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): def execute(self, context):
if self.layer_obj == None: if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.") self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'} return {'FINISHED'}
if self.is_lost: if is_missing_layer(self.layer_obj):
self.report({"WARNING"}, "Selected layer is lost.") self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'} return {'FINISHED'}
self.get_node_tree(self.node_type, self.node_link) bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
prev_context = bpy.context.area.type node_name="FetchLayer",
bpy.context.area.type = 'NODE_EDITOR' node_label="Fetch Layer")
bpy.ops.node.add_node('INVOKE_DEFAULT',
type='GeometryNodeGroup',
use_transform=True)
bpy.context.area.type = prev_context
node = bpy.context.active_node node = bpy.context.active_node
self.assign_node_tree(node) node.inputs[0].default_value = self.layer_obj
node.show_options = False node.label = get_layer_label(self.layer_obj)
layer_obj = self.layer_obj
node.inputs[0].default_value = layer_obj
node.label = get_layer_label(layer_obj)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event):
self.nodes_file = custom_nodes.nodes_file def get_selected_layers(context, layer_filter=None):
self.node_type = "BioxelNodes_FetchLayer" def _layer_filter(layer_obj, context):
return self.execute(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): def get_selected_layer(context, layer_filter=None):
bl_idname = "BIOXELNODES_MT_ADD_LAYER" layer_objs = get_selected_layers(context, layer_filter)
bl_label = "Fetch Layer" return layer_objs[0] if len(layer_objs) > 0 else None
def draw(self, context):
container_obj = get_container_obj(bpy.context.active_object)
layer_objs = get_container_layer_objs(container_obj)
layout = self.layout
for layer_obj in layer_objs:
op = layout.operator(FetchLayer.bl_idname,
text=get_layer_item_label(context, layer_obj))
op.layer_obj_name = layer_obj.name
class ModifyLayerOperator(LayerOperator): class OutputLayerOperator():
def layer_operate(self, orig_layer: Layer): new_layer_name: bpy.props.StringProperty(name="New Name",
options={"SKIP_SAVE"}) # type: ignore
def operate(self, orig_layer: Layer, context):
"""do the operation""" """do the operation"""
return orig_layer return orig_layer
def add_layer_node(self, context, layer): def add_layer_node(self, context, layer):
layer_obj = layer_to_obj(layer, layer_obj = layer_to_obj(layer,
container_obj=self.layer_obj.parent, container_obj=context.object,
cache_dir=get_cache_dir(context)) cache_dir=get_cache_dir())
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT', bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
layer_obj_name=layer_obj.name) layer_obj_name=layer_obj.name)
def execute(self, context): 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.") self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'} return {'FINISHED'}
if self.is_lost: if is_missing_layer(layer_obj):
self.report({"WARNING"}, "Selected layer is lost.") self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'} return {'FINISHED'}
orig_layer = obj_to_layer(self.layer_obj) orig_layer = obj_to_layer(layer_obj)
new_layer = self.layer_operate(orig_layer) 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) self.add_layer_node(context, new_layer)
return {'FINISHED'} return {'FINISHED'}
class ResampleScalar(bpy.types.Operator, ModifyLayerOperator): class LayerOperator():
bl_idname = "bioxelnodes.resample_scalar"
bl_label = "Resample Scalar" def operate(self, layer_obj: bpy.types.Object, context):
bl_description = "Resample Scalar" """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" 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( bioxel_size: bpy.props.FloatProperty(
name="Bioxel Size", name="Bioxel Size",
soft_min=0.1, soft_max=10.0, 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[1]*orig_size/new_size),
int(orig_shape[2]*orig_size/new_size)) int(orig_shape[2]*orig_size/new_size))
def layer_operate(self, orig_layer: Layer): def operate(self, orig_layer: Layer, context):
modified_layer = orig_layer.copy() new_layer = orig_layer.copy()
new_shape = self.get_new_shape(modified_layer.shape, new_shape = self.get_new_shape(new_layer.shape,
modified_layer.bioxel_size[0], new_layer.bioxel_size[0],
self.bioxel_size) self.bioxel_size)
modified_layer.resize(new_shape) new_layer.resize(new_shape, self.smooth)
modified_layer.name = f"{orig_layer.name}_R-{self.bioxel_size:.2f}" new_layer.name = self.new_layer_name \
return modified_layer or f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
return new_layer
def draw(self, context): def draw(self, context):
orig_shape = get_layer_prop_value(self.layer_obj, "shape") layer_obj = get_selected_layer(context)
orig_size = get_layer_prop_value(self.layer_obj, "bioxel_size") 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, new_shape = self.get_new_shape(orig_shape,
orig_size, orig_size,
self.bioxel_size) self.bioxel_size)
@@ -186,45 +295,49 @@ class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
layer_shape_text += "**TOO LARGE!**" layer_shape_text += "**TOO LARGE!**"
layout = self.layout layout = self.layout
layout.prop(self, "new_layer_name")
layout.prop(self, "bioxel_size") layout.prop(self, "bioxel_size")
layout.prop(self, "smooth")
layout.label(text=layer_shape_text) layout.label(text=layer_shape_text)
def invoke(self, context, event): def invoke(self, context, event):
if self.layer_obj: layer_obj = get_selected_layer(context)
bioxel_size = get_layer_prop_value(self.layer_obj, "bioxel_size") if layer_obj:
name = get_layer_label(layer_obj)
self.bioxel_size = bioxel_size self.bioxel_size = get_layer_prop_value(layer_obj,
"bioxel_size")
context.window_manager.invoke_props_dialog(self, context.window_manager.invoke_props_dialog(self,
width=400, title=f"Resample {name}")
title=f"Resample {self.layer_obj.name}")
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
else: else:
return self.execute(context) return self.execute(context)
class SignScalar(bpy.types.Operator, ModifyLayerOperator): class SignScalar(bpy.types.Operator, OutputLayerOperator):
bl_idname = "bioxelnodes.sign_scalar" bl_idname = "bioxelnodes.sign_scalar"
bl_label = "Sign Scalar" bl_label = "Sign Value"
bl_description = "Sign the scalar value" bl_description = "Sign value"
bl_icon = "REMOVE" bl_icon = "REMOVE"
def layer_operate(self, orig_layer: Layer): def operate(self, orig_layer: Layer, context):
modified_layer = orig_layer.copy() new_layer = orig_layer.copy()
modified_layer.data = -orig_layer.data new_layer.data = -orig_layer.data
modified_layer.name = f"{orig_layer.name}_Sign" new_layer.name = self.new_layer_name \
return modified_layer or f"{orig_layer.name}_Sign"
return new_layer
def invoke(self, context, event): def invoke(self, context, event):
if self.layer_obj: layer_obj = get_selected_layer(context)
context.window_manager.invoke_confirm(self, if layer_obj:
event, name = get_layer_label(layer_obj)
message=f"Are you sure to sign {self.layer_obj.name}?") context.window_manager.invoke_props_dialog(self,
title=f"Sign {name}")
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
else: else:
return self.execute(context) return self.execute(context)
class FillOperator(ModifyLayerOperator): class FillOperator(OutputLayerOperator):
fill_value: bpy.props.FloatProperty( fill_value: bpy.props.FloatProperty(
name="Fill Value", name="Fill Value",
@@ -238,13 +351,13 @@ class FillOperator(ModifyLayerOperator):
) # type: ignore ) # type: ignore
def invoke(self, context, event): def invoke(self, context, event):
if self.layer_obj: layer_obj = get_selected_layer(context)
scalar_min = get_layer_prop_value(self.layer_obj, "min") if layer_obj:
scalar_min = get_layer_prop_value(layer_obj, "min")
self.fill_value = min(scalar_min, 0) self.fill_value = min(scalar_min, 0)
name = get_layer_label(layer_obj)
context.window_manager.invoke_props_dialog(self, context.window_manager.invoke_props_dialog(self,
width=400, title=f"Fill {name}")
title=f"Fill {self.layer_obj.name}")
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
else: else:
return self.execute(context) return self.execute(context)
@@ -253,7 +366,7 @@ class FillOperator(ModifyLayerOperator):
class FillByThreshold(bpy.types.Operator, FillOperator): class FillByThreshold(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_threshold" bl_idname = "bioxelnodes.fill_by_threshold"
bl_label = "Fill Value 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" bl_icon = "EMPTY_SINGLE_ARROW"
threshold: bpy.props.FloatProperty( threshold: bpy.props.FloatProperty(
@@ -262,21 +375,22 @@ class FillByThreshold(bpy.types.Operator, FillOperator):
default=128, default=128,
) # type: ignore ) # type: ignore
def layer_operate(self, orig_layer: Layer): def operate(self, orig_layer: Layer, context):
data = np.amax(orig_layer.data, -1) data = np.amax(orig_layer.data, -1)
mask = data <= self.threshold \ mask = data <= self.threshold \
if self.invert else data > self.threshold if self.invert else data > self.threshold
modified_layer = orig_layer.copy() new_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask) new_layer.fill(self.fill_value, mask, 3)
modified_layer.name = f"{orig_layer.name}_F-{self.threshold}" new_layer.name = self.new_layer_name \
return modified_layer or f"{orig_layer.name}_F-{self.threshold:.2f}"
return new_layer
class FillByRange(bpy.types.Operator, FillOperator): class FillByRange(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_range" bl_idname = "bioxelnodes.fill_by_range"
bl_label = "Fill Value by Range" bl_label = "Fill Value by Range"
bl_description = "Fill Value by Range" bl_description = "Fill value by range"
bl_icon = "IPO_CONSTANT" bl_icon = "IPO_CONSTANT"
from_min: bpy.props.FloatProperty( from_min: bpy.props.FloatProperty(
@@ -291,82 +405,112 @@ class FillByRange(bpy.types.Operator, FillOperator):
default=256, default=256,
) # type: ignore ) # type: ignore
def layer_operate(self, orig_layer: Layer): def operate(self, orig_layer: Layer, context):
data = np.amax(orig_layer.data, -1) data = np.amax(orig_layer.data, -1)
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \ mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
(data > self.from_min) & (data < self.from_max) (data > self.from_min) & (data < self.from_max)
modified_layer = orig_layer.copy() new_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask) new_layer.fill(self.fill_value, mask, 3)
modified_layer.name = f"{orig_layer.name}_F-{self.from_min}-{self.from_max}" new_layer.name = self.new_layer_name \
return modified_layer or f"{orig_layer.name}_F-{self.from_min:.2f}-{self.from_max:.2f}"
return new_layer
class FillByLabel(bpy.types.Operator, FillOperator): class FillByLabel(bpy.types.Operator, FillOperator):
bl_idname = "bioxelnodes.fill_by_label" bl_idname = "bioxelnodes.fill_by_label"
bl_label = "Fill Value 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" 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", label_obj_name: bpy.props.EnumProperty(name="Label Layer",
items=get_label_layer_selection) # type: ignore 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) label_obj = bpy.data.objects.get(self.label_obj_name)
if not label_obj: if label_obj is None:
self.report({"WARNING"}, "Cannot find any label layer.") raise NoContent("Cannot find any label layer.")
return {'FINISHED'}
label_layer = obj_to_layer(label_obj) 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) mask = np.amax(label_layer.data, -1)
if self.invert: if self.invert:
mask = 1 - mask mask = 1 - mask
modified_layer = orig_layer.copy() new_layer = orig_layer.copy()
modified_layer.fill(self.fill_value, mask) new_layer.fill(self.fill_value, mask, 3)
modified_layer.name = f"{orig_layer.name}_F-{label_layer.name}" new_layer.name = self.new_layer_name \
return modified_layer 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_idname = "bioxelnodes.combine_labels"
bl_label = "Combine Labels" bl_label = "Combine Labels"
bl_description = "Combine all selected labels" bl_description = "Combine all selected labels"
bl_icon = "MOD_BUILD" bl_icon = "MOD_BUILD"
def execute(self, context): def execute(self, context):
label_objs = [obj for obj in get_selected_objs_in_node_tree(context) def layer_filter(layer_obj, context):
if get_layer_prop_value(obj, "kind") == "label"] return get_layer_kind(layer_obj) == "label"
label_objs = get_selected_layers(context, layer_filter)
if len(label_objs) < 2: if len(label_objs) < 2:
self.report({"WARNING"}, "Not enough layers.") self.report({"WARNING"}, "Not enough layers.")
return {'FINISHED'} return {'FINISHED'}
base_obj = label_objs[0] base_obj = label_objs[0]
label_objs = label_objs[1:] label_objs = label_objs[1:]
base_layer = obj_to_layer(base_obj) base_layer = obj_to_layer(base_obj)
modified_layer = base_layer.copy() new_layer = base_layer.copy()
label_names = [base_layer.name] label_names = [base_layer.name]
for label_obj in label_objs: for label_obj in label_objs:
label_layer = obj_to_layer(label_obj) label_layer = obj_to_layer(label_obj)
label_layer.resize(base_layer.shape) label_layer.resize(base_layer.shape)
modified_layer.data = np.maximum( new_layer.data = np.maximum(
modified_layer.data, label_layer.data) new_layer.data, label_layer.data)
label_names.append(label_layer.name) 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'} return {'FINISHED'}
class SaveLayerCache(bpy.types.Operator, LayerOperator): class BatchLayerOperator():
bl_idname = "bioxelnodes.save_layer_cache" success_msg = "Successfully done selected layers."
bl_label = "Save Layer Cache" fail_msg = "fails."
bl_description = "Save Layer Cache"
bl_icon = "FILE_TICK" 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( cache_dir: bpy.props.StringProperty(
name="Cache Directory", name="Cache Directory",
@@ -374,23 +518,14 @@ class SaveLayerCache(bpy.types.Operator, LayerOperator):
default="//" default="//"
) # type: ignore ) # type: ignore
def execute(self, context): def operate(self, layer_obj, context):
if self.layer_obj == None:
self.report({"WARNING"}, "Get no layer.")
return {'FINISHED'}
if self.is_lost:
self.report({"WARNING"}, "Selected layer is lost.")
return {'FINISHED'}
# "//"
output_dir = bpy.path.abspath(self.cache_dir) 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() 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() \ output_path: Path = Path(output_dir, name, source_path.name).resolve() \
if is_sequence else Path(output_dir, 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() blend_path = Path(bpy.path.abspath("//")).resolve()
self.layer_obj.data.filepath = bpy.path.relpath(str(output_path), layer_obj.data.filepath = bpy.path.relpath(str(output_path),
start=str(blend_path)) start=str(blend_path))
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context, event):
if self.layer_obj: context.window_manager.invoke_props_dialog(self)
context.window_manager.invoke_props_dialog(self,
width=500)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
else:
return self.execute(context)
class RenameLayer(bpy.types.Operator, LayerOperator): class RemoveLayers(BatchLayerOperator):
bl_idname = "bioxelnodes.rename_layer" fail_msg = "fail to remove."
bl_label = "Rename Layer"
bl_description = "Rename Layer"
bl_icon = "FILE_FONT"
name: bpy.props.StringProperty(name="New Name") # type: ignore def operate(self, layer_obj, context):
for node_group in bpy.data.node_groups:
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: for node in node_group.nodes:
if get_node_type(node) == "BioxelNodes_FetchLayer": if get_node_type(node) == "BioxelNodes_FetchLayer":
if node.inputs[0].default_value == self.layer_obj: if node.inputs[0].default_value == 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) node_group.nodes.remove(node)
cache_filepath = Path(bpy.path.abspath( cache_filepath = Path(bpy.path.abspath(
self.layer_obj.data.filepath)).resolve() layer_obj.data.filepath)).resolve()
if cache_filepath.is_file(): 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(): for f in cache_filepath.parent.iterdir():
f.unlink(missing_ok=True) f.unlink(missing_ok=True)
else: else:
cache_filepath.unlink(missing_ok=True) cache_filepath.unlink(missing_ok=True)
# also remove layer object # also remove layer object
bpy.data.volumes.remove(self.layer_obj.data) bpy.data.volumes.remove(layer_obj.data)
return {'FINISHED'} 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): def invoke(self, context, event):
context.window_manager.invoke_confirm(self, context.window_manager.invoke_confirm(self,
event, event,
message=f"Are you sure to remove all **Missing** layers?") message=f"Are you sure to remove them?")
return {'RUNNING_MODAL'} 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 import bpy
from pathlib import Path from pathlib import Path
import shutil 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): class ReLinkNodeLib(bpy.types.Operator):
bl_idname = "bioxelnodes.relink_nodes" bl_idname = "bioxelnodes.relink_node_lib"
bl_label = "Relink Nodes to Addon" bl_label = "Relink Node Library"
bl_description = "Relink all nodes to addon source" 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): def execute(self, context):
file_name = Path(custom_nodes.nodes_file).name lib_filepath = Path(NODE_LIB_FILEPATH.parent,
for lib in bpy.data.libraries: f"{self.node_lib_filename}.blend")
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve() node_libs = []
lib_name = lib_path.name for node_group in bpy.data.node_groups:
if lib_name == file_name: if node_group.name.startswith("BioxelNodes"):
if str(lib_path) != custom_nodes.nodes_file: node_lib = node_group.library
lib.filepath = custom_nodes.nodes_file 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.") self.report({"INFO"}, f"Successfully relinked.")
return {'FINISHED'} return {'FINISHED'}
class SaveStagedData(bpy.types.Operator): class SaveNodeLib(bpy.types.Operator):
bl_idname = "bioxelnodes.save_staged_data" bl_idname = "bioxelnodes.save_node_lib"
bl_label = "Save Staged Data" bl_label = "Save Node Library"
bl_description = "Save all staged data in this file for sharing" bl_description = "Save node library file to local"
bl_options = {'UNDO'}
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
lib_dir: bpy.props.StringProperty( lib_dir: bpy.props.StringProperty(
name="Library Directory", name="Library Directory",
@@ -57,98 +54,66 @@ class SaveStagedData(bpy.types.Operator):
) # type: ignore ) # type: ignore
def execute(self, context): 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))
for file in files:
file_name = Path(file).name
# "//"
lib_dir = bpy.path.abspath(self.lib_dir) lib_dir = bpy.path.abspath(self.lib_dir)
local_lib_path: Path = Path(lib_dir, NODE_LIB_FILEPATH.name).resolve()
output_path: Path = Path(lib_dir, file_name).resolve() addon_lib_path: Path = NODE_LIB_FILEPATH
source_path: Path = Path(file).resolve()
if output_path != source_path:
shutil.copy(source_path, output_path)
for lib in bpy.data.libraries:
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
if lib_path == source_path:
blend_path = Path(bpy.path.abspath("//")).resolve() 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 local_lib_path != addon_lib_path:
shutil.copy(addon_lib_path, local_lib_path)
if self.save_cache: libs = []
fails = [] for node_group in bpy.data.node_groups:
for layer_obj in get_all_layer_objs(): if node_group.name.startswith("BioxelNodes"):
try: if node_group.library:
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT', libs.append(node_group.library)
layer_obj_name=layer_obj.name,
cache_dir=self.cache_dir)
except:
fails.append(layer_obj)
if len(fails) == 0: libs = list(set(libs))
self.report({"INFO"}, f"Successfully saved bioxel layers.") for lib in libs:
else: lib.filepath = bpy.path.relpath(str(local_lib_path),
self.report( start=str(blend_path))
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} fail to save.")
set_file_prop("addon_version", VERSION)
return {'FINISHED'} return {'FINISHED'}
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self, context.window_manager.invoke_props_dialog(self)
width=500)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return bpy.data.is_saved 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 CleanTemp(bpy.types.Operator):
class CleanAllCaches(bpy.types.Operator): bl_idname = "bioxelnodes.clear_temp"
bl_idname = "bioxelnodes.clear_all_caches" bl_label = "Clean Temp"
bl_label = "Clean All Caches in Temp" bl_description = "Clean all cache in temp (include other project cache)"
bl_description = "Clean all caches saved in temp"
def execute(self, context): def execute(self, context):
cache_dir = get_cache_dir(context) cache_dir = get_cache_dir()
try: try:
shutil.rmtree(cache_dir) shutil.rmtree(cache_dir)
self.report({"INFO"}, f"Successfully cleaned caches.") self.report({"INFO"}, f"Successfully cleaned temp.")
return {'FINISHED'} return {'FINISHED'}
except: except:
self.report({"WARNING"}, self.report({"WARNING"},
"Fail to clean caches, you may do it manually.") "Fail to clean temp, you may do it manually.")
return {'CANCELLED'} return {'CANCELLED'}
def invoke(self, context, event): def invoke(self, context, event):
context.window_manager.invoke_confirm(self, context.window_manager.invoke_confirm(self,
event, 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'} return {'RUNNING_MODAL'}
class RenderSettingPreset(bpy.types.Operator): class RenderSettingPreset(bpy.types.Operator):
bl_idname = "bioxelnodes.render_setting_preset" bl_idname = "bioxelnodes.render_setting_preset"
bl_label = "Render Setting Preset" bl_label = "Render Setting Presets"
bl_description = "Render Setting Preset" bl_description = "Render setting presets for bioxel"
bl_options = {'UNDO'}
PRESETS = { PRESETS = {
"preview_e": "Preview (EEVEE)", "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.transparent_max_bounces = 16
bpy.context.scene.cycles.volume_preview_step_rate = 1 bpy.context.scene.cycles.volume_preview_step_rate = 1
bpy.context.scene.cycles.volume_step_rate = 1 bpy.context.scene.cycles.volume_step_rate = 1
# bpy.context.scene.cycles.use_fast_gi = True
elif self.preset == "production_c": elif self.preset == "production_c":
bpy.context.scene.render.engine = 'CYCLES' bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.shading_system = True bpy.context.scene.cycles.shading_system = True
bpy.context.scene.cycles.volume_bounces = 16 bpy.context.scene.cycles.volume_bounces = 16
bpy.context.scene.cycles.transparent_max_bounces = 32 bpy.context.scene.cycles.transparent_max_bounces = 32
bpy.context.scene.cycles.volume_preview_step_rate = 0.1 bpy.context.scene.cycles.volume_preview_step_rate = 0.5
bpy.context.scene.cycles.volume_step_rate = 0.1 bpy.context.scene.cycles.volume_step_rate = 0.5
# bpy.context.scene.cycles.use_fast_gi = False
return {'FINISHED'} return {'FINISHED'}
class SliceViewer(bpy.types.Operator): class SliceViewer(bpy.types.Operator):
bl_idname = "bioxelnodes.slice_viewer" bl_idname = "bioxelnodes.slice_viewer"
bl_label = "Slice Viewer" bl_label = "Slice Viewer"
bl_description = "Slice Viewer" bl_description = "A preview scene setting for viewing slicers"
bl_icon = "FILE_VOLUME" bl_icon = "FILE_VOLUME"
def execute(self, context): def execute(self, context):
@@ -224,5 +192,47 @@ class SliceViewer(bpy.types.Operator):
area.spaces[0].shading.use_scene_lights = False area.spaces[0].shading.use_scene_lights = False
area.spaces[0].shading.use_scene_world = 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 import bpy
from pathlib import Path from pathlib import Path
class BioxelNodesPreferences(bpy.types.AddonPreferences): class BioxelNodesPreferences(bpy.types.AddonPreferences):
bl_idname = __package__ bl_idname = __package__
@@ -10,14 +11,19 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences):
default=str(Path(Path.home(), '.bioxelnodes')) default=str(Path(Path.home(), '.bioxelnodes'))
) # type: ignore ) # type: ignore
do_change_render_setting: bpy.props.BoolProperty( change_render_setting: bpy.props.BoolProperty(
name="Change Render Setting", name="Change Render Setting on First Import",
default=True, default=True,
) # type: ignore ) # 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): def draw(self, context):
layout = self.layout layout = self.layout
layout.label(text="Configuration") layout.label(text="Configuration")
layout.prop(self, 'cache_dir') 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 import bpy
from .bioxelutils.utils import get_node_type from .bioxelutils.common import get_node_type
class BIOXELNODES_UL_layer_list(bpy.types.UIList): 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: if node.inputs[0].default_value == layer_obj:
node.select = True node.select = True
layer_list_UL.layer_list_active = -1
class BIOXELNODES_Layer(bpy.types.PropertyGroup): class BIOXELNODES_Layer(bpy.types.PropertyGroup):
obj_name: bpy.props.StringProperty() # type: ignore obj_name: bpy.props.StringProperty() # type: ignore

View File

@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import shutil import shutil
import bpy
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True): 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(): if not target_path.exists():
raise Exception 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"

View File

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