feat: improving the user experience
This commit is contained in:
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.2.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.2.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.3.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.3.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
Binary file not shown.
@@ -2,7 +2,6 @@ import copy
|
||||
import numpy as np
|
||||
|
||||
from . import scipy
|
||||
from . import skimage as ski
|
||||
from . import scipy as ndi
|
||||
|
||||
# 3rd-party
|
||||
@@ -70,23 +69,27 @@ class Layer():
|
||||
def copy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
def fill(self, value: float, mask: np.ndarray):
|
||||
def fill(self, value: float, mask: np.ndarray, smooth: int = 0):
|
||||
mask_frames = ()
|
||||
if mask.ndim == 4:
|
||||
if mask.shape[0] != self.frame_count:
|
||||
raise Exception("Mask frame count is not same as ")
|
||||
for f in range(self.frame_count):
|
||||
mask_frame = mask[f, :, :, :]
|
||||
mask_frame = scipy.minimum_filter(
|
||||
mask_frame.astype(np.float32), size=3)
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
elif mask.ndim == 3:
|
||||
for f in range(self.frame_count):
|
||||
mask_frame = mask[:, :, :]
|
||||
mask_frame = scipy.minimum_filter(
|
||||
mask_frame.astype(np.float32), size=3)
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
@@ -97,12 +100,11 @@ class Layer():
|
||||
_mask = np.expand_dims(_mask, axis=-1)
|
||||
self.data = _mask * value + (1-_mask) * self.data
|
||||
|
||||
def resize(self, shape: tuple, progress_callback=None):
|
||||
def resize(self, shape: tuple, smooth: int = 0, progress_callback=None):
|
||||
if len(shape) != 3:
|
||||
raise Exception("Shape must be 3 dim")
|
||||
|
||||
data = self.data
|
||||
order = 0 if self.dtype == bool else 1
|
||||
|
||||
# # TXYZC > TXYZ
|
||||
# if self.kind in ['label', 'scalar']:
|
||||
@@ -121,15 +123,22 @@ class Layer():
|
||||
# shape,
|
||||
# preserve_range=True,
|
||||
# anti_aliasing=data.dtype.kind != "b")
|
||||
frame = data[f, :, :, :, :]
|
||||
if smooth > 0:
|
||||
frame = scipy.median_filter(frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
|
||||
factors = np.divide(self.shape, shape)
|
||||
zoom_factors = [1 / f for f in factors]
|
||||
frame = ndi.zoom(data[f, :, :, :, :],
|
||||
order = 0 if frame.dtype == bool else 1
|
||||
frame = ndi.zoom(frame,
|
||||
zoom_factors+[1.0],
|
||||
mode="nearest",
|
||||
grid_mode=False,
|
||||
order=order)
|
||||
|
||||
if smooth > 0:
|
||||
frame = frame.astype(self.dtype)
|
||||
data_frames += (frame,)
|
||||
|
||||
data = np.stack(data_frames)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
from .layer import Layer
|
||||
|
||||
# 3rd-party
|
||||
import SimpleITK as sitk
|
||||
@@ -166,7 +165,7 @@ def remove_end_str(string: str, end: str):
|
||||
return string
|
||||
|
||||
|
||||
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None) -> Layer:
|
||||
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None):
|
||||
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
||||
|
||||
Args:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
from ast import literal_eval
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||
node.location.x = target_node.location.x + offset[0]
|
||||
@@ -48,20 +53,65 @@ def get_container_obj(current_obj):
|
||||
return None
|
||||
|
||||
|
||||
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
|
||||
def get_layer_prop_value(layer_obj: bpy.types.Object, prop_name: str):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
value = layer_node.inputs[prop].default_value
|
||||
prop = layer_node.inputs.get(prop_name)
|
||||
if prop is None:
|
||||
return None
|
||||
|
||||
value = prop.default_value
|
||||
|
||||
if type(value).__name__ == "bpy_prop_array":
|
||||
value = tuple(value)
|
||||
return tuple([int(v) for v in value]) \
|
||||
if prop in ["shape"] else value
|
||||
elif type(value).__name__ == "str":
|
||||
return str(value)
|
||||
if type(value).__name__ == "float":
|
||||
elif type(value).__name__ == "float":
|
||||
value = float(value)
|
||||
return round(value, 2) \
|
||||
if prop in ["bioxel_size"] else value
|
||||
elif type(value).__name__ == "int":
|
||||
value = int(value)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def get_layer_name(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "name")
|
||||
|
||||
|
||||
def get_layer_kind(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "kind")
|
||||
|
||||
|
||||
def get_layer_label(layer_obj):
|
||||
name = get_layer_name(layer_obj)
|
||||
# kind = get_layer_kind(layer_obj)
|
||||
|
||||
label = f"{name}"
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
return "**MISSING**" + label
|
||||
elif is_temp_layer(layer_obj):
|
||||
return "* " + label
|
||||
else:
|
||||
return label
|
||||
|
||||
|
||||
def is_missing_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
return not cache_filepath.is_file()
|
||||
|
||||
|
||||
def is_temp_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
cache_dirpath = Path(get_cache_dir())
|
||||
return cache_dirpath in cache_filepath.parents
|
||||
|
||||
|
||||
def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value):
|
||||
@@ -95,6 +145,7 @@ def get_all_layer_objs():
|
||||
|
||||
return layer_objs
|
||||
|
||||
|
||||
def add_driver(target, target_prop, var_sources, expression):
|
||||
driver = target.driver_add(target_prop)
|
||||
is_vector = isinstance(driver, list)
|
||||
@@ -125,4 +176,70 @@ def add_direct_driver(target, target_prop, source, source_prop):
|
||||
}
|
||||
]
|
||||
expression = "var0"
|
||||
add_driver(target, target_prop, drivers, expression)
|
||||
add_driver(target, target_prop, drivers, expression)
|
||||
|
||||
|
||||
def read_file_prop(content: str):
|
||||
props = {}
|
||||
for line in content.split("\n"):
|
||||
line = line.replace(" ", "")
|
||||
p = line.split("=")[0]
|
||||
if p != "":
|
||||
v = line.split("=")[-1]
|
||||
props[p] = v
|
||||
return props
|
||||
|
||||
|
||||
def write_file_prop(props: dict):
|
||||
lines = []
|
||||
for p, v in props.items():
|
||||
lines.append(f"{p} = {v}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def set_file_prop(prop, value):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
props[prop] = value
|
||||
bpy.data.texts["BioxelNodes"].clear()
|
||||
bpy.data.texts["BioxelNodes"].write(write_file_prop(props))
|
||||
|
||||
|
||||
def get_file_prop(prop):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
return props.get(prop)
|
||||
|
||||
|
||||
def is_incompatible():
|
||||
if get_file_prop("addon_version") is None:
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
return True
|
||||
else:
|
||||
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||
if addon_version[0] != VERSION[0]\
|
||||
or addon_version[1] != VERSION[1]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def local_lib_not_updated():
|
||||
use_local = False
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_group_lib = node_group.library
|
||||
if node_group_lib:
|
||||
abs_filepath = bpy.path.abspath(node_group_lib.filepath)
|
||||
_local_lib_file = Path(abs_filepath).resolve().as_posix()
|
||||
if _local_lib_file != NODE_LIB_FILEPATH.as_posix():
|
||||
use_local = True
|
||||
break
|
||||
|
||||
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||
not_update = addon_version != VERSION
|
||||
return use_local and not_update
|
||||
@@ -6,12 +6,12 @@ from mathutils import Matrix, Vector
|
||||
|
||||
|
||||
from .layer import Layer, layer_to_obj, obj_to_layer
|
||||
from ..nodes import custom_nodes
|
||||
from .utils import (get_container_layer_objs,
|
||||
get_layer_prop_value,
|
||||
get_nodes_by_type,
|
||||
move_node_to_node)
|
||||
|
||||
from .common import (get_container_layer_objs,
|
||||
get_layer_prop_value,
|
||||
get_nodes_by_type,
|
||||
move_node_to_node)
|
||||
from .node import add_node_to_graph
|
||||
from ..utils import get_use_link
|
||||
|
||||
NODE_TYPE = {
|
||||
"label": "BioxelNodes_MaskByLabel",
|
||||
@@ -82,13 +82,17 @@ def add_layers(layers: list[Layer],
|
||||
cache_dir: str):
|
||||
|
||||
node_group = container_obj.modifiers[0].node_group
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
try:
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
except:
|
||||
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
for i, layer in enumerate(layers):
|
||||
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
|
||||
fetch_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_FetchLayer")
|
||||
fetch_node = add_node_to_graph("FetchLayer",
|
||||
node_group,
|
||||
get_use_link())
|
||||
fetch_node.label = get_layer_prop_value(layer_obj, "name")
|
||||
fetch_node.inputs[0].default_value = layer_obj
|
||||
|
||||
@@ -104,6 +108,7 @@ def add_layers(layers: list[Layer],
|
||||
|
||||
def container_to_obj(container: Container,
|
||||
scene_scale: float,
|
||||
step_size: float,
|
||||
cache_dir: str):
|
||||
# Wrapper a Container
|
||||
|
||||
@@ -134,11 +139,26 @@ def container_to_obj(container: Container,
|
||||
container_obj.name = container.name
|
||||
container_obj.data.name = container.name
|
||||
container_obj.show_in_front = True
|
||||
|
||||
container_obj.lock_location[0] = True
|
||||
container_obj.lock_location[1] = True
|
||||
container_obj.lock_location[2] = True
|
||||
|
||||
container_obj.lock_rotation[0] = True
|
||||
container_obj.lock_rotation[1] = True
|
||||
container_obj.lock_rotation[2] = True
|
||||
|
||||
container_obj.lock_scale[0] = True
|
||||
container_obj.lock_scale[1] = True
|
||||
container_obj.lock_scale[2] = True
|
||||
|
||||
container_obj['bioxel_container'] = True
|
||||
container_obj["scene_scale"] = scene_scale
|
||||
container_obj["step_size"] = step_size
|
||||
|
||||
modifier = container_obj.modifiers.new("GeometryNodes", 'NODES')
|
||||
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
|
||||
node_group.interface.new_socket(name="Component",
|
||||
node_group.interface.new_socket(name="Output",
|
||||
in_out="OUTPUT",
|
||||
socket_type="NodeSocketGeometry")
|
||||
modifier.node_group = node_group
|
||||
|
||||
@@ -7,10 +7,11 @@ import pyopenvdb as vdb
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from ..nodes import custom_nodes
|
||||
from ..bioxel.layer import Layer
|
||||
from .utils import (get_layer_prop_value,
|
||||
move_node_between_nodes, add_direct_driver)
|
||||
from ..utils import get_use_link
|
||||
from .node import add_node_to_graph
|
||||
from .common import (get_layer_prop_value,
|
||||
move_node_between_nodes)
|
||||
|
||||
|
||||
def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
@@ -26,7 +27,11 @@ def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
grids, base_metadata = vdb.readAll(str(f))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
data_frame = np.ndarray(grid["data_shape"], np.float32)
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data_frame = np.ndarray(data_shape, np.float32)
|
||||
grid.copyToArray(data_frame)
|
||||
data_frames += (data_frame,)
|
||||
data = np.stack(data_frames)
|
||||
@@ -34,7 +39,11 @@ def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
grids, base_metadata = vdb.readAll(str(cache_filepath))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
data = np.ndarray(grid["data_shape"], np.float32)
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data = np.ndarray(data_shape, np.float32)
|
||||
grid.copyToArray(data)
|
||||
data = np.expand_dims(data, axis=0) # expend frame
|
||||
|
||||
@@ -137,13 +146,12 @@ def layer_to_obj(layer: Layer,
|
||||
|
||||
layer_data = bpy.data.volumes.new(layer_display_name)
|
||||
layer_data.render.space = 'WORLD'
|
||||
layer_data.render.step_size = container_obj.scale[0] * layer.bioxel_size[0]
|
||||
scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
step_size = container_obj.get("step_size") or 1
|
||||
layer_data.render.step_size = scene_scale * step_size
|
||||
layer_data.sequence_mode = 'REPEAT'
|
||||
layer_data.filepath = str(cache_filepaths[0])
|
||||
|
||||
# add_direct_driver(layer_data, "render.step_size",
|
||||
# container_obj, "scale[0]")
|
||||
|
||||
if layer.frame_count > 1:
|
||||
layer_data.is_sequence = True
|
||||
layer_data.frame_duration = layer.frame_count
|
||||
@@ -164,8 +172,7 @@ def layer_to_obj(layer: Layer,
|
||||
socket_type="NodeSocketGeometry")
|
||||
modifier.node_group = node_group
|
||||
|
||||
layer_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes__Layer")
|
||||
layer_node = add_node_to_graph("_Layer", node_group, get_use_link())
|
||||
|
||||
layer_node.inputs['name'].default_value = layer.name
|
||||
layer_node.inputs['shape'].default_value = layer.shape
|
||||
@@ -176,11 +183,12 @@ def layer_to_obj(layer: Layer,
|
||||
affine_key = f"affine{i}{j}"
|
||||
layer_node.inputs[affine_key].default_value = layer.affine[j, i]
|
||||
|
||||
layer_node.inputs['id'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
layer_node.inputs['unique'].default_value = random.uniform(0, 1)
|
||||
layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0]
|
||||
layer_node.inputs['dtype'].default_value = layer.dtype.str
|
||||
layer_node.inputs['dtype_num'].default_value = layer.dtype.num
|
||||
layer_node.inputs['frame_count'].default_value = layer.frame_count
|
||||
layer_node.inputs['channel_count'].default_value = layer.channel_count
|
||||
layer_node.inputs['offset'].default_value = max(0, -layer.min)
|
||||
layer_node.inputs['min'].default_value = layer.min
|
||||
layer_node.inputs['max'].default_value = layer.max
|
||||
|
||||
68
bioxelnodes/bioxelutils/node.py
Normal file
68
bioxelnodes/bioxelutils/node.py
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "bioxelnodes"
|
||||
version = "0.4.0"
|
||||
version = "1.0.0"
|
||||
name = "Bioxel Nodes"
|
||||
tagline = "For scientific volumetric data visualization in Blender"
|
||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
|
||||
from pathlib import Path
|
||||
from .customnodes import CustomNodes
|
||||
|
||||
NODE_FILE = "BioxelNodes_4.2"
|
||||
VERSION = (1, 0, 0)
|
||||
NODE_LIB_FILENAME = "BioxelNodes_v1.0.x"
|
||||
NODE_LIB_FILEPATH = Path(Path(__file__).parent,
|
||||
f"assets/Nodes/{NODE_LIB_FILENAME}.blend").resolve()
|
||||
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
@@ -12,34 +13,40 @@ MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Cutout by Threshold',
|
||||
'icon': 'EMPTY_SINGLE_ARROW',
|
||||
'node_type': 'BioxelNodes_CutoutByThreshold',
|
||||
'node_description': ''
|
||||
'name': 'CutoutByThreshold',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Range',
|
||||
'icon': 'IPO_CONSTANT',
|
||||
'node_type': 'BioxelNodes_CutoutByRange',
|
||||
'node_description': ''
|
||||
'name': 'CutoutByRange',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Color',
|
||||
'icon': 'COLOR',
|
||||
'node_type': 'BioxelNodes_CutoutByColor',
|
||||
'node_description': ''
|
||||
'name': 'CutoutByColor',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'To Surface',
|
||||
'icon': 'MESH_DATA',
|
||||
'node_type': 'BioxelNodes_ToSurface',
|
||||
'node_description': ''
|
||||
'name': 'ToSurface',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Join Component',
|
||||
'icon': 'CONSTRAINT_BONE',
|
||||
'node_type': 'BioxelNodes_JoinComponent',
|
||||
'node_description': ''
|
||||
'name': 'JoinComponent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slice',
|
||||
'icon': 'TEXTURE',
|
||||
'name': 'Slice',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -49,59 +56,46 @@ MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Set Properties',
|
||||
'icon': 'PROPERTIES',
|
||||
'node_type': 'BioxelNodes_SetProperties',
|
||||
'node_description': ''
|
||||
'name': 'SetProperties',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Set Color',
|
||||
'icon': 'IPO_SINE',
|
||||
'node_type': 'BioxelNodes_SetColor',
|
||||
'node_description': ''
|
||||
'name': 'SetColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Color',
|
||||
'icon': 'IPO_QUINT',
|
||||
'node_type': 'BioxelNodes_SetColorByColor',
|
||||
'node_description': ''
|
||||
'name': 'SetColorByColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 2',
|
||||
'icon': 'IPO_QUAD',
|
||||
'node_type': 'BioxelNodes_SetColorByRamp2',
|
||||
'node_description': ''
|
||||
'name': 'SetColorByRamp2',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 3',
|
||||
'icon': 'IPO_CUBIC',
|
||||
'node_type': 'BioxelNodes_SetColorByRamp3',
|
||||
'node_description': ''
|
||||
'name': 'SetColorByRamp3',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 4',
|
||||
'icon': 'IPO_QUART',
|
||||
'node_type': 'BioxelNodes_SetColorByRamp4',
|
||||
'node_description': ''
|
||||
'name': 'SetColorByRamp4',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 5',
|
||||
'icon': 'IPO_QUINT',
|
||||
'node_type': 'BioxelNodes_SetColorByRamp5',
|
||||
'node_description': ''
|
||||
},
|
||||
# "separator",
|
||||
# {
|
||||
# 'label': 'Color Presets',
|
||||
# 'icon': 'COLOR',
|
||||
# 'node_type': 'BioxelNodes_ColorPresets',
|
||||
# 'node_description': ''
|
||||
# }
|
||||
# {
|
||||
# 'label': 'Color Presets MRI',
|
||||
# 'icon': 'COLOR',
|
||||
# 'node_type': 'BioxelNodes_ColorPresets_MRI',
|
||||
# 'node_description': ''
|
||||
# },
|
||||
'name': 'SetColorByRamp5',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -111,39 +105,45 @@ MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Membrane Shader',
|
||||
'icon': 'NODE_MATERIAL',
|
||||
'node_type': 'BioxelNodes_AssignMembraneShader',
|
||||
'node_description': ''
|
||||
'name': 'AssignMembraneShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Solid Shader',
|
||||
'icon': 'SHADING_SOLID',
|
||||
'node_type': 'BioxelNodes_AssignSolidShader',
|
||||
'node_description': ''
|
||||
'name': 'AssignSolidShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slime Shader',
|
||||
'icon': 'OUTLINER_DATA_META',
|
||||
'node_type': 'BioxelNodes_AssignSlimeShader',
|
||||
'node_description': ''
|
||||
'name': 'AssignSlimeShader',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Inflate',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'node_type': 'BioxelNodes_M_Inflate',
|
||||
'node_description': ''
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'name': 'Transform',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Smooth',
|
||||
'icon': 'MOD_SMOOTH',
|
||||
'node_type': 'BioxelNodes_M_Smooth',
|
||||
'node_description': ''
|
||||
'label': 'Transform Parent',
|
||||
'icon': 'ORIENTATION_PARENT',
|
||||
'name': 'TransformParent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Remove Small Island',
|
||||
'icon': 'FORCE_LENNARDJONES',
|
||||
'node_type': 'BioxelNodes_M_RemoveSmallIsland',
|
||||
'node_description': ''
|
||||
'label': 'ReCenter',
|
||||
'icon': 'PROP_CON',
|
||||
'name': 'ReCenter',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -154,21 +154,21 @@ MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Cut',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'node_type': 'BioxelNodes_Cut',
|
||||
'node_description': ''
|
||||
'name': 'Cut',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Primitive Cutter',
|
||||
'icon': 'MOD_LINEART',
|
||||
'node_type': 'BioxelNodes_PrimitiveCutter',
|
||||
'node_description': ''
|
||||
'name': 'PrimitiveCutter',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Object Cutter',
|
||||
'icon': 'MESH_PLANE',
|
||||
'node_type': 'BioxelNodes_ObjectCutter',
|
||||
'node_description': ''
|
||||
'name': 'ObjectCutter',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -177,41 +177,48 @@ MENU_ITEMS = [
|
||||
'icon': 'MODIFIER',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Pick Mesh',
|
||||
'label': 'Pick Surface',
|
||||
'icon': 'OUTLINER_OB_MESH',
|
||||
'node_type': 'BioxelNodes_PickMesh',
|
||||
'node_description': ''
|
||||
'name': 'PickSurface',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Volume',
|
||||
'icon': 'OUTLINER_OB_VOLUME',
|
||||
'node_type': 'BioxelNodes_PickVolume',
|
||||
'node_description': ''
|
||||
'name': 'PickVolume',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Shape Wire',
|
||||
'icon': 'FILE_VOLUME',
|
||||
'name': 'PickShapeWire',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Pick Bbox Wire',
|
||||
'icon': 'MESH_CUBE',
|
||||
'node_type': 'BioxelNodes_PickBboxWire',
|
||||
'node_description': ''
|
||||
'name': 'PickBboxWire',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Inflate',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'name': 'Inflate',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Smooth',
|
||||
'icon': 'MOD_SMOOTH',
|
||||
'name': 'Smooth',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Remove Small Island',
|
||||
'icon': 'FORCE_LENNARDJONES',
|
||||
'name': 'RemoveSmallIsland',
|
||||
'description': ''
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
custom_nodes = CustomNodes(
|
||||
menu_items=MENU_ITEMS,
|
||||
nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend",
|
||||
class_prefix="BIOXELNODES_MT_NODES",
|
||||
root_label='Bioxel Nodes',
|
||||
root_icon="FILE_VOLUME",
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
custom_nodes.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
custom_nodes.unregister()
|
||||
@@ -1 +0,0 @@
|
||||
from .menus import CustomNodes
|
||||
@@ -1,147 +0,0 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
from .nodes import AddCustomNode
|
||||
|
||||
|
||||
class CustomNodes():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
nodes_file,
|
||||
class_prefix="CUSTOMNODES_MT_NODES",
|
||||
root_label='CustomNodes',
|
||||
root_icon='NONE'
|
||||
) -> None:
|
||||
if not Path(nodes_file).is_file():
|
||||
raise FileNotFoundError(str(Path(nodes_file).resolve()))
|
||||
|
||||
self.menu_items = menu_items
|
||||
self.nodes_file = str(Path(nodes_file).resolve())
|
||||
self.class_prefix = class_prefix
|
||||
self.root_label = root_label
|
||||
self.root_icon = root_icon
|
||||
self.class_prefix = class_prefix
|
||||
|
||||
menu_classes = []
|
||||
self._create_menu_class(
|
||||
items=menu_items,
|
||||
label=root_label,
|
||||
menu_classes=menu_classes
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"{class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
|
||||
def add_node_menu(self, context):
|
||||
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(idname, icon=root_icon)
|
||||
|
||||
self.add_node_menu = add_node_menu
|
||||
|
||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
||||
nodes_file = self.nodes_file
|
||||
idname_namespace = idname_namespace or self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
for item in items:
|
||||
item_items = item.get('items') if item != 'separator' else None
|
||||
if item_items:
|
||||
menu_class = self._create_menu_class(
|
||||
menu_classes=menu_classes,
|
||||
items=item_items,
|
||||
label=item.get('label') or 'CustomNodes',
|
||||
icon=item.get('icon') or 0,
|
||||
idname_namespace=idname
|
||||
)
|
||||
item['menu_class'] = menu_class
|
||||
|
||||
# create menu class
|
||||
class Menu(bpy.types.Menu):
|
||||
bl_idname = idname
|
||||
bl_label = label
|
||||
nodes_file = self.nodes_file
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
for item in items:
|
||||
# print(item)
|
||||
if item == "separator":
|
||||
layout.separator()
|
||||
elif item.get('menu_class'):
|
||||
layout.menu(
|
||||
item.get('menu_class').bl_idname,
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
else:
|
||||
op = layout.operator(
|
||||
'customnodes.add_custom_node',
|
||||
text=item.get('label'),
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
op.nodes_file = nodes_file
|
||||
op.node_type = item['node_type']
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_description = item.get(
|
||||
'node_description') or "Add Custom Node."
|
||||
op.node_link = item.get('link') or True
|
||||
op.node_callback = item.get('node_callback') or ""
|
||||
|
||||
menu_classes.append(Menu)
|
||||
return Menu
|
||||
|
||||
def _find_item(self, found_items, menu_items, node_type: str):
|
||||
|
||||
for item in menu_items:
|
||||
if item == 'separator':
|
||||
continue
|
||||
|
||||
if item.get("node_type") == node_type:
|
||||
found_items.append(item)
|
||||
|
||||
item_items = item.get('items')
|
||||
if item_items:
|
||||
self._find_item(found_items, item_items, node_type)
|
||||
|
||||
def find_item(self, node_type: str):
|
||||
found_items = []
|
||||
self._find_item(found_items, self.menu_items, node_type)
|
||||
return found_items[0] if len(found_items) > 0 else None
|
||||
|
||||
def add_node(self, node_group, node_type: str):
|
||||
item = self.find_item(node_type)
|
||||
op = AddCustomNode()
|
||||
op.nodes_file = self.nodes_file
|
||||
op.node_type = node_type
|
||||
if item:
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_link = item.get('link') or True
|
||||
op.node_callback = item.get('node_callback') or ""
|
||||
else:
|
||||
op.node_label = ""
|
||||
op.node_link = True
|
||||
op.node_callback = ""
|
||||
return op.add_node(node_group)
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.NODE_MT_add.append(self.add_node_menu)
|
||||
bpy.types.Scene.customnodes_node_dir = bpy.props.StringProperty(
|
||||
name="Node Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
)
|
||||
|
||||
def unregister(self):
|
||||
bpy.types.NODE_MT_add.remove(self.add_node_menu)
|
||||
try:
|
||||
for cls in reversed(self.menu_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
except RuntimeError:
|
||||
pass
|
||||
@@ -1,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"}
|
||||
@@ -2,3 +2,21 @@ class CancelledByUser(Exception):
|
||||
def __init__(self):
|
||||
message = 'Cancelled by user'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class NoContent(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class NoFound(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class Incompatible(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
@@ -1,46 +1,70 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from .operators.utils import get_layer_item_label
|
||||
from .bioxelutils.utils import (get_container_obj,
|
||||
get_container_objs_from_selection,
|
||||
get_container_layer_objs,
|
||||
get_layer_prop_value)
|
||||
from .operators.layer import (FetchLayerMenu, FetchLayer,
|
||||
RemoveLayer, RemoveMissingLayers, RenameLayer, ResampleScalar, SaveLayerCache,
|
||||
SignScalar, CombineLabels,
|
||||
FillByLabel, FillByThreshold, FillByRange, get_selected_objs_in_node_tree)
|
||||
from .operators.container import (SaveAllLayerCaches, SaveContainer, LoadContainer,
|
||||
from .constants import MENU_ITEMS, NODE_LIB_FILENAME
|
||||
from .node_menu import NodeMenu
|
||||
|
||||
from .bioxelutils.common import (get_container_obj,
|
||||
get_container_layer_objs, get_layer_label,
|
||||
get_layer_prop_value)
|
||||
from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer,
|
||||
RemoveSelectedLayers, SaveSelectedLayersCache,
|
||||
ResampleLayer, SignScalar, CombineLabels,
|
||||
FillByLabel, FillByThreshold, FillByRange)
|
||||
from .operators.container import (AddLocator, AddSlicer, ExtractShapeWire,
|
||||
SaveContainerLayersCache, RemoveContainerMissingLayers,
|
||||
SaveContainer, LoadContainer,
|
||||
AddPieCutter, AddPlaneCutter,
|
||||
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
|
||||
PickBboxWire, PickMesh, PickVolume, ScaleContainer)
|
||||
ExtractBboxWire, ExtractSurface, ExtractVolume, ContainerProps)
|
||||
|
||||
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
|
||||
from .operators.misc import (CleanAllCaches,
|
||||
ReLinkNodes, RenderSettingPreset, SaveStagedData, SliceViewer)
|
||||
from .operators.misc import (CleanTemp,
|
||||
ReLinkNodeLib, RemoveAllMissingLayers, RenderSettingPreset, SaveAllLayersCache, SaveNodeLib, SliceViewer)
|
||||
|
||||
|
||||
class PickFromContainerMenu(bpy.types.Menu):
|
||||
class FetchLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_ADD_LAYER"
|
||||
bl_label = "Fetch Layer"
|
||||
|
||||
def draw(self, context):
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
layout = self.layout
|
||||
|
||||
for layer_obj in layer_objs:
|
||||
op = layout.operator(FetchLayer.bl_idname,
|
||||
text=get_layer_label(layer_obj))
|
||||
op.layer_obj_name = layer_obj.name
|
||||
|
||||
|
||||
class ExtractFromContainerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_PICK"
|
||||
bl_label = "Pick from Container"
|
||||
bl_label = "Extract from Container"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(PickMesh.bl_idname)
|
||||
layout.operator(PickVolume.bl_idname)
|
||||
layout.operator(PickBboxWire.bl_idname)
|
||||
layout.operator(ExtractSurface.bl_idname)
|
||||
layout.operator(ExtractVolume.bl_idname)
|
||||
layout.operator(ExtractShapeWire.bl_idname)
|
||||
layout.operator(ExtractBboxWire.bl_idname)
|
||||
|
||||
|
||||
class AddCutterMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
||||
bl_label = "Add a Object Cutter"
|
||||
bl_label = "Add a Cutter"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(AddPlaneCutter.bl_idname, text="Plane Cutter")
|
||||
layout.operator(AddCylinderCutter.bl_idname, text="Cylinder Cutter")
|
||||
layout.operator(AddCubeCutter.bl_idname, text="Cube Cutter")
|
||||
layout.operator(AddSphereCutter.bl_idname, text="Sphere Cutter")
|
||||
layout.operator(AddPieCutter.bl_idname, text="Pie Cutter")
|
||||
layout.operator(AddPlaneCutter.bl_idname,
|
||||
icon=AddPlaneCutter.bl_icon, text="Plane Cutter")
|
||||
layout.operator(AddCylinderCutter.bl_idname,
|
||||
icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter")
|
||||
layout.operator(AddCubeCutter.bl_idname,
|
||||
icon=AddCubeCutter.bl_icon, text="Cube Cutter")
|
||||
layout.operator(AddSphereCutter.bl_idname,
|
||||
icon=AddSphereCutter.bl_icon, text="Sphere Cutter")
|
||||
layout.operator(AddPieCutter.bl_idname,
|
||||
icon=AddPieCutter.bl_icon, text="Pie Cutter")
|
||||
|
||||
|
||||
class ImportLayerMenu(bpy.types.Menu):
|
||||
@@ -60,7 +84,7 @@ class ImportLayerMenu(bpy.types.Menu):
|
||||
|
||||
class AddLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_ADDLAYER"
|
||||
bl_label = "Import Volumetric Data (Add to)"
|
||||
bl_label = "Import Volumetric Data (Add)"
|
||||
bl_icon = "FILE_NEW"
|
||||
|
||||
def draw(self, context):
|
||||
@@ -79,31 +103,37 @@ class ModifyLayerMenu(bpy.types.Menu):
|
||||
bl_icon = "FILE_NEW"
|
||||
|
||||
def draw(self, context):
|
||||
layer_objs = get_selected_objs_in_node_tree(context)
|
||||
if len(layer_objs) > 0:
|
||||
active_obj_name = layer_objs[0].name
|
||||
else:
|
||||
active_obj_name = ""
|
||||
|
||||
layout = self.layout
|
||||
op = layout.operator(ResampleScalar.bl_idname,
|
||||
icon=ResampleScalar.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(SignScalar.bl_idname,
|
||||
icon=SignScalar.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(FillByThreshold.bl_idname,
|
||||
icon=FillByThreshold.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(FillByRange.bl_idname,
|
||||
icon=FillByRange.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(FillByLabel.bl_idname,
|
||||
icon=FillByLabel.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(CombineLabels.bl_idname,
|
||||
icon=CombineLabels.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
layout.operator(SignScalar.bl_idname,
|
||||
icon=SignScalar.bl_icon)
|
||||
layout.operator(FillByThreshold.bl_idname,
|
||||
icon=FillByThreshold.bl_icon)
|
||||
layout.operator(FillByRange.bl_idname,
|
||||
icon=FillByRange.bl_icon)
|
||||
layout.operator(FillByLabel.bl_idname,
|
||||
icon=FillByLabel.bl_icon)
|
||||
layout.operator(CombineLabels.bl_idname,
|
||||
icon=CombineLabels.bl_icon)
|
||||
|
||||
|
||||
class ReLinkNodeLibMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_RELINK"
|
||||
bl_label = "Relink Node Library"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
versions = [{"label": "v0.3.x", "filename": "BioxelNodes_v0.3.x"},
|
||||
{"label": "v0.2.x", "filename": "BioxelNodes_v0.2.x"},
|
||||
{"label": "v0.1.x", "filename": "BioxelNodes_v0.1.x"}]
|
||||
|
||||
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||
text="Current Version")
|
||||
op.node_lib_filename = NODE_LIB_FILENAME
|
||||
layout.separator()
|
||||
for version in versions:
|
||||
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||
text=version["label"])
|
||||
op.node_lib_filename = version["filename"]
|
||||
|
||||
|
||||
class RenderSettingMenu(bpy.types.Menu):
|
||||
@@ -118,6 +148,15 @@ class RenderSettingMenu(bpy.types.Menu):
|
||||
op.preset = k
|
||||
|
||||
|
||||
class DangerZoneMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_DANGER"
|
||||
bl_label = "Danger Zone"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(CleanTemp.bl_idname)
|
||||
|
||||
|
||||
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_TOPBAR"
|
||||
bl_label = "Bioxel Nodes"
|
||||
@@ -130,9 +169,15 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
layout.operator(LoadContainer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveStagedData.bl_idname)
|
||||
layout.operator(ReLinkNodes.bl_idname)
|
||||
layout.operator(CleanAllCaches.bl_idname)
|
||||
layout.operator(SaveAllLayersCache.bl_idname)
|
||||
layout.operator(RemoveAllMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveNodeLib.bl_idname)
|
||||
layout.menu(ReLinkNodeLibMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(DangerZoneMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(RenderSettingMenu.bl_idname)
|
||||
@@ -143,51 +188,66 @@ def TOPBAR(self, context):
|
||||
layout.menu(BioxelNodesTopbarMenu.bl_idname)
|
||||
|
||||
|
||||
class BioxelNodesNodeMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE"
|
||||
class NodeHeadMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_HEAD"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
layer_objs = get_selected_objs_in_node_tree(context)
|
||||
if len(layer_objs) > 0:
|
||||
active_obj_name = layer_objs[0].name
|
||||
else:
|
||||
active_obj_name = ""
|
||||
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(AddLayerMenu.bl_idname,
|
||||
icon=AddLayerMenu.bl_icon)
|
||||
layout.separator()
|
||||
layout.operator(SaveContainer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ScaleContainer.bl_idname)
|
||||
layout.operator(ContainerProps.bl_idname)
|
||||
layout.menu(ExtractFromContainerMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveContainerLayersCache.bl_idname,
|
||||
icon=SaveContainerLayersCache.bl_icon)
|
||||
layout.operator(RemoveContainerMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(AddLocator.bl_idname,
|
||||
icon=AddLocator.bl_icon)
|
||||
layout.operator(AddSlicer.bl_idname,
|
||||
icon=AddSlicer.bl_icon)
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
layout.menu(PickFromContainerMenu.bl_idname)
|
||||
layout.operator(SaveAllLayerCaches.bl_idname,
|
||||
icon=SaveAllLayerCaches.bl_icon)
|
||||
layout.operator(RemoveMissingLayers.bl_idname,
|
||||
icon=RemoveMissingLayers.bl_icon)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(FetchLayerMenu.bl_idname)
|
||||
op = layout.operator(SaveLayerCache.bl_idname,
|
||||
icon=SaveLayerCache.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = layout.operator(RenameLayer.bl_idname,
|
||||
icon=RenameLayer.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
|
||||
op = layout.operator(RemoveLayer.bl_idname,
|
||||
icon=RemoveLayer.bl_icon)
|
||||
op.layer_obj_name = active_obj_name
|
||||
|
||||
class NodeContextMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_CONTEXT"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SaveSelectedLayersCache.bl_idname,
|
||||
icon=SaveSelectedLayersCache.bl_icon)
|
||||
layout.operator(RemoveSelectedLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(ModifyLayerMenu.bl_idname)
|
||||
layout.operator(RenameLayer.bl_idname,
|
||||
icon=RenameLayer.bl_icon)
|
||||
layout.operator(RetimeLayer.bl_idname)
|
||||
layout.operator(RelocateLayer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ResampleLayer.bl_idname,
|
||||
icon=ResampleLayer.bl_icon)
|
||||
layout.operator(SignScalar.bl_idname)
|
||||
layout.operator(FillByThreshold.bl_idname)
|
||||
layout.operator(FillByRange.bl_idname)
|
||||
layout.operator(FillByLabel.bl_idname)
|
||||
layout.operator(CombineLabels.bl_idname)
|
||||
|
||||
|
||||
def NODE(self, context):
|
||||
def NODE_CONTEXT(self, context):
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
@@ -197,10 +257,23 @@ def NODE(self, context):
|
||||
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(BioxelNodesNodeMenu.bl_idname)
|
||||
layout.menu(NodeContextMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_PT(self, context):
|
||||
def NODE_HEAD(self, context):
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
|
||||
if not is_geo_nodes or not is_container:
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(NodeHeadMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_PROP(self, context):
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
@@ -216,20 +289,19 @@ def NODE_PT(self, context):
|
||||
|
||||
layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
|
||||
layer_list = layer_list_UL.layer_list
|
||||
layer_list_active = layer_list_UL.layer_list_active
|
||||
layer_list.clear()
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
layer_item = layer_list.add()
|
||||
layer_item.label = get_layer_item_label(context, layer_obj)
|
||||
layer_item.label = get_layer_label(layer_obj)
|
||||
layer_item.obj_name = layer_obj.name
|
||||
layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
|
||||
for prop in ["kind", "bioxel_size", "shape", "min", "max", ]])
|
||||
|
||||
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list):
|
||||
active_obj_name = layer_list[layer_list_active].obj_name
|
||||
else:
|
||||
active_obj_name = ""
|
||||
for prop in ["kind",
|
||||
"bioxel_size",
|
||||
"shape",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
layout = self.layout
|
||||
layout.label(text="Layer List")
|
||||
@@ -243,48 +315,41 @@ def NODE_PT(self, context):
|
||||
item_dyntip_propname="info_text",
|
||||
rows=20)
|
||||
|
||||
sidebar = split.column(align=True)
|
||||
sidebar.menu(AddLayerMenu.bl_idname,
|
||||
icon=AddLayerMenu.bl_icon, text="")
|
||||
# sidebar = split.column(align=True)
|
||||
# sidebar.menu(AddLayerMenu.bl_idname,
|
||||
# icon=AddLayerMenu.bl_icon, text="")
|
||||
|
||||
sidebar.operator(SaveAllLayerCaches.bl_idname,
|
||||
icon=SaveAllLayerCaches.bl_icon, text="")
|
||||
sidebar.operator(RemoveMissingLayers.bl_idname,
|
||||
icon=RemoveMissingLayers.bl_icon, text="")
|
||||
# sidebar.operator(SaveContainerLayersCache.bl_idname,
|
||||
# icon=SaveContainerLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveContainerMissingLayers.bl_idname,
|
||||
# icon=RemoveContainerMissingLayers.bl_icon, text="")
|
||||
|
||||
sidebar.separator()
|
||||
op = sidebar.operator(FetchLayer.bl_idname,
|
||||
icon=FetchLayer.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(SaveLayerCache.bl_idname,
|
||||
icon=SaveLayerCache.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(RenameLayer.bl_idname,
|
||||
icon=RenameLayer.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(RemoveLayer.bl_idname,
|
||||
icon=RemoveLayer.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SaveSelectedLayersCache.bl_idname,
|
||||
# icon=SaveSelectedLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveSelectedLayers.bl_idname,
|
||||
# icon=RemoveSelectedLayers.bl_icon, text="")
|
||||
|
||||
sidebar.separator()
|
||||
op = sidebar.operator(ResampleScalar.bl_idname,
|
||||
icon=ResampleScalar.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(SignScalar.bl_idname,
|
||||
icon=SignScalar.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(FillByThreshold.bl_idname,
|
||||
icon=FillByThreshold.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(FillByRange.bl_idname,
|
||||
icon=FillByRange.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
op = sidebar.operator(FillByLabel.bl_idname,
|
||||
icon=FillByLabel.bl_icon, text="")
|
||||
op.layer_obj_name = active_obj_name
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(RenameLayer.bl_idname,
|
||||
# icon=RenameLayer.bl_icon, text="")
|
||||
# sidebar.operator(ResampleLayer.bl_idname,
|
||||
# icon=ResampleLayer.bl_icon, text="")
|
||||
# sidebar.operator(RetimeLayer.bl_idname,
|
||||
# icon=RetimeLayer.bl_icon, text="")
|
||||
|
||||
sidebar.separator()
|
||||
layout.separator()
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SignScalar.bl_idname,
|
||||
# icon=SignScalar.bl_icon, text="")
|
||||
# sidebar.operator(FillByThreshold.bl_idname,
|
||||
# icon=FillByThreshold.bl_icon, text="")
|
||||
# sidebar.operator(FillByRange.bl_idname,
|
||||
# icon=FillByRange.bl_icon, text="")
|
||||
# sidebar.operator(FillByLabel.bl_idname,
|
||||
# icon=FillByLabel.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# layout.separator()
|
||||
|
||||
|
||||
def VIEW3D_TOPBAR(self, context):
|
||||
@@ -293,15 +358,24 @@ def VIEW3D_TOPBAR(self, context):
|
||||
icon=SliceViewer.bl_icon, text="")
|
||||
|
||||
|
||||
node_menu = NodeMenu(
|
||||
menu_items=MENU_ITEMS
|
||||
)
|
||||
|
||||
|
||||
def add():
|
||||
node_menu.register()
|
||||
bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PT)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
||||
bpy.types.NODE_MT_editor_menus.append(NODE)
|
||||
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.append(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT)
|
||||
|
||||
|
||||
def remove():
|
||||
bpy.types.VIEW3D_HT_header.remove(VIEW3D_TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.remove(NODE_PT)
|
||||
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
||||
bpy.types.NODE_MT_editor_menus.remove(NODE)
|
||||
bpy.types.NODE_PT_node_tree_properties.remove(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.remove(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT)
|
||||
node_menu.unregister()
|
||||
|
||||
87
bioxelnodes/node_menu.py
Normal file
87
bioxelnodes/node_menu.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import bpy
|
||||
from .operators.node import AddNode
|
||||
|
||||
|
||||
class NodeMenu():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
) -> None:
|
||||
|
||||
self.menu_items = menu_items
|
||||
self.class_prefix = f"BIOXELNODES_MT"
|
||||
root_label = "Bioxel Nodes"
|
||||
menu_classes = []
|
||||
self._create_menu_class(
|
||||
items=menu_items,
|
||||
label=root_label,
|
||||
menu_classes=menu_classes
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"{self.class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
icon = "FILE_VOLUME"
|
||||
|
||||
def drew_menu(self, context):
|
||||
if (bpy.context.area.spaces[0].tree_type == 'GeometryNodeTree'):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(idname,
|
||||
icon=icon)
|
||||
self.drew_menu = drew_menu
|
||||
|
||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
||||
idname_namespace = idname_namespace or self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
for item in items:
|
||||
item_items = item.get('items') if item != 'separator' else None
|
||||
if item_items:
|
||||
menu_class = self._create_menu_class(
|
||||
menu_classes=menu_classes,
|
||||
items=item_items,
|
||||
label=item.get('label') or 'CustomNodes',
|
||||
icon=item.get('icon') or 0,
|
||||
idname_namespace=idname
|
||||
)
|
||||
item['menu_class'] = menu_class
|
||||
|
||||
# create menu class
|
||||
class Menu(bpy.types.Menu):
|
||||
bl_idname = idname
|
||||
bl_label = label
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for item in items:
|
||||
if item == "separator":
|
||||
layout.separator()
|
||||
elif item.get('menu_class'):
|
||||
layout.menu(
|
||||
item.get('menu_class').bl_idname,
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
else:
|
||||
op = layout.operator(
|
||||
AddNode.bl_idname,
|
||||
text=item.get('label'),
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
op.node_name = item['name']
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_description = item.get(
|
||||
'node_description') or ""
|
||||
|
||||
menu_classes.append(Menu)
|
||||
return Menu
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.NODE_MT_add.append(self.drew_menu)
|
||||
|
||||
def unregister(self):
|
||||
bpy.types.NODE_MT_add.remove(self.drew_menu)
|
||||
for cls in reversed(self.menu_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -2,17 +2,16 @@ import bpy
|
||||
|
||||
import bmesh
|
||||
|
||||
from ..nodes import custom_nodes
|
||||
from ..customnodes.nodes import AddCustomNode
|
||||
from ..utils import get_cache_dir, get_use_link, select_object
|
||||
from ..bioxel.io import load_container, save_container
|
||||
from ..bioxelutils.container import container_to_obj, obj_to_container
|
||||
from ..bioxelutils.utils import (get_container_layer_objs, get_container_obj,
|
||||
get_layer_prop_value, get_nodes_by_type,
|
||||
move_node_between_nodes,
|
||||
move_node_to_node,
|
||||
get_all_layer_objs)
|
||||
|
||||
from .utils import get_cache_dir, select_object
|
||||
from ..bioxelutils.node import add_node_to_graph
|
||||
from ..bioxelutils.common import (get_container_layer_objs, get_container_obj,
|
||||
get_nodes_by_type, is_missing_layer,
|
||||
move_node_between_nodes,
|
||||
move_node_to_node,
|
||||
get_all_layer_objs)
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
|
||||
class SaveContainer(bpy.types.Operator):
|
||||
@@ -63,7 +62,7 @@ class LoadContainer(bpy.types.Operator):
|
||||
|
||||
container_obj = container_to_obj(container,
|
||||
scene_scale=0.01,
|
||||
cache_dir=get_cache_dir(context))
|
||||
cache_dir=get_cache_dir())
|
||||
select_object(container_obj)
|
||||
|
||||
if is_first_import:
|
||||
@@ -78,7 +77,7 @@ class LoadContainer(bpy.types.Operator):
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class PickObject():
|
||||
class ExtractObject():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
@@ -92,18 +91,19 @@ class PickObject():
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
obj = bpy.context.active_object
|
||||
|
||||
obj.name = f"{self.object_type}_{container_obj.name}"
|
||||
obj.name = f"{container_obj.name}_{self.object_type}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
pick_mesh_node = custom_nodes.add_node(node_group,
|
||||
f"BioxelNodes_Pick{self.object_type}")
|
||||
fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
pick_mesh_node.inputs[0].default_value = container_obj
|
||||
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
||||
fetch_mesh_node.inputs[0].default_value = container_obj
|
||||
node_group.links.new(fetch_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
select_object(obj)
|
||||
|
||||
@@ -112,26 +112,34 @@ class PickObject():
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PickMesh(bpy.types.Operator, PickObject):
|
||||
bl_idname = "bioxelnodes.pick_mesh"
|
||||
bl_label = "Pick Mesh"
|
||||
bl_description = "Pick Container Mesh"
|
||||
class ExtractSurface(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.fetch_mesh"
|
||||
bl_label = "Extract Surface"
|
||||
bl_description = "Extract Surface"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
object_type = "Mesh"
|
||||
object_type = "Surface"
|
||||
|
||||
|
||||
class PickVolume(bpy.types.Operator, PickObject):
|
||||
class ExtractVolume(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_volume"
|
||||
bl_label = "Pick Volume"
|
||||
bl_description = "Pick Container Volume"
|
||||
bl_label = "Extract Volume"
|
||||
bl_description = "Extract Volume"
|
||||
bl_icon = "OUTLINER_OB_VOLUME"
|
||||
object_type = "Volume"
|
||||
|
||||
|
||||
class PickBboxWire(bpy.types.Operator, PickObject):
|
||||
class ExtractShapeWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_shape_wire"
|
||||
bl_label = "Extract Shape Wire"
|
||||
bl_description = "Extract Shape Wire"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
object_type = "ShapeWire"
|
||||
|
||||
|
||||
class ExtractBboxWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
||||
bl_label = "Pick Bbox Wire"
|
||||
bl_description = "Pick Container Bbox Wire"
|
||||
bl_label = "Extract Bbox Wire"
|
||||
bl_description = "Extract Bbox Wire"
|
||||
bl_icon = "MESH_CUBE"
|
||||
object_type = "BboxWire"
|
||||
|
||||
@@ -162,14 +170,12 @@ class AddCutter():
|
||||
elif self.cutter_type == "pie":
|
||||
bpy.ops.mesh.primitive_plane_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
pie = bpy.context.active_object
|
||||
pie.name = "Pie"
|
||||
orig_data = pie.data
|
||||
pie_obj = bpy.context.active_object
|
||||
orig_data = pie_obj.data
|
||||
|
||||
# Create mesh
|
||||
pie_mesh = bpy.data.meshes.new('Pie')
|
||||
|
||||
pie.data = pie_mesh
|
||||
pie_obj.data = pie_mesh
|
||||
bpy.data.meshes.remove(orig_data)
|
||||
|
||||
# # Create object
|
||||
@@ -204,7 +210,19 @@ class AddCutter():
|
||||
# bpy.context.view_layer.objects.active = pie
|
||||
|
||||
cutter_obj = bpy.context.active_object
|
||||
cutter_obj.location = container_obj.location
|
||||
|
||||
# vcos = [container_obj.matrix_world @
|
||||
# v.co for v in container_obj.data.vertices]
|
||||
|
||||
# def find_center(l): return (max(l) + min(l)) / 2
|
||||
|
||||
# x, y, z = [[v[i] for v in vcos] for i in range(3)]
|
||||
# center = [find_center(axis) for axis in [x, y, z]]
|
||||
|
||||
# cutter_obj.location = center
|
||||
name = self.cutter_type.capitalize()
|
||||
cutter_obj.name = name
|
||||
cutter_obj.data.name = name
|
||||
cutter_obj.visible_camera = False
|
||||
cutter_obj.visible_diffuse = False
|
||||
cutter_obj.visible_glossy = False
|
||||
@@ -213,6 +231,7 @@ class AddCutter():
|
||||
cutter_obj.visible_shadow = False
|
||||
cutter_obj.hide_render = True
|
||||
cutter_obj.display_type = 'WIRE'
|
||||
cutter_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
@@ -222,13 +241,16 @@ class AddCutter():
|
||||
cut_nodes = get_nodes_by_type(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
if len(cut_nodes) == 0:
|
||||
cutter_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_ObjectCutter")
|
||||
cutter_node = add_node_to_graph("ObjectCutter",
|
||||
node_group,
|
||||
get_use_link())
|
||||
cutter_node.inputs[0].default_value = self.cutter_type.capitalize()
|
||||
cutter_node.inputs[1].default_value = cutter_obj
|
||||
|
||||
cut_node = custom_nodes.add_node(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
cut_node = add_node_to_graph("Cut",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
@@ -249,58 +271,20 @@ class AddCutter():
|
||||
|
||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||
else:
|
||||
bpy.ops.bioxelnodes.object_cutter('INVOKE_DEFAULT',
|
||||
cutter_obj_name=cutter_obj.name,
|
||||
cutter_type=self.cutter_type.capitalize())
|
||||
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||
node_name="ObjectCutter",
|
||||
node_label=name)
|
||||
node = bpy.context.active_node
|
||||
node.inputs[0].default_value = name
|
||||
node.inputs[1].default_value = cutter_obj
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ObjectCutter(bpy.types.Operator, AddCustomNode):
|
||||
bl_idname = "bioxelnodes.object_cutter"
|
||||
bl_label = "Object Cutter"
|
||||
bl_description = "Object Cutter"
|
||||
bl_icon = "NODE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
cutter_obj_name: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
cutter_type: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
cutter_obj = bpy.data.objects.get(self.cutter_obj_name)
|
||||
if cutter_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.get_node_tree(self.node_type, self.node_link)
|
||||
prev_context = bpy.context.area.type
|
||||
bpy.context.area.type = 'NODE_EDITOR'
|
||||
bpy.ops.node.add_node('INVOKE_DEFAULT',
|
||||
type='GeometryNodeGroup',
|
||||
use_transform=True)
|
||||
bpy.context.area.type = prev_context
|
||||
node = bpy.context.active_node
|
||||
|
||||
self.assign_node_tree(node)
|
||||
node.show_options = False
|
||||
|
||||
node.label = "Object Cutter"
|
||||
node.inputs[0].default_value = self.cutter_type
|
||||
node.inputs[1].default_value = cutter_obj
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.nodes_file = custom_nodes.nodes_file
|
||||
self.node_type = "BioxelNodes_ObjectCutter"
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class AddPlaneCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_plane_cutter"
|
||||
bl_label = "Add a Plane Cutter"
|
||||
bl_description = "Add a Plane Cutter to Container"
|
||||
bl_description = "Add a plane cutter to current container"
|
||||
bl_icon = "MESH_PLANE"
|
||||
cutter_type = "plane"
|
||||
|
||||
@@ -308,7 +292,7 @@ class AddPlaneCutter(bpy.types.Operator, AddCutter):
|
||||
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
||||
bl_label = "Add a Cylinder Cutter"
|
||||
bl_description = "Add a Cylinder Cutter to Container"
|
||||
bl_description = "Add a cylinder cutter to current container"
|
||||
bl_icon = "MESH_CYLINDER"
|
||||
cutter_type = "cylinder"
|
||||
|
||||
@@ -316,7 +300,7 @@ class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
||||
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cube_cutter"
|
||||
bl_label = "Add a Cube Cutter"
|
||||
bl_description = "Add a Cube Cutter to Container"
|
||||
bl_description = "Add a cube cutter to current container"
|
||||
bl_icon = "MESH_CUBE"
|
||||
cutter_type = "cube"
|
||||
|
||||
@@ -324,7 +308,7 @@ class AddCubeCutter(bpy.types.Operator, AddCutter):
|
||||
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_sphere_cutter"
|
||||
bl_label = "Add a Sphere Cutter"
|
||||
bl_description = "Add a Sphere Cutter to Container"
|
||||
bl_description = "Add a sphere cutter to current container"
|
||||
bl_icon = "MESH_UVSPHERE"
|
||||
cutter_type = "sphere"
|
||||
|
||||
@@ -332,15 +316,128 @@ class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||
class AddPieCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_pie_cutter"
|
||||
bl_label = "Add a Pie Cutter"
|
||||
bl_description = "Add a Pie Cutter to Container"
|
||||
bl_description = "Add a pie cutter to current container"
|
||||
bl_icon = "MESH_CONE"
|
||||
cutter_type = "pie"
|
||||
|
||||
|
||||
class ScaleContainer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.scale_container"
|
||||
bl_label = "Scale Container"
|
||||
bl_description = "Scale Container."
|
||||
class AddSlicer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_slicer"
|
||||
bl_label = "Add a Slicer"
|
||||
bl_description = "Add a slicer to current container"
|
||||
bl_icon = "TEXTURE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=2,
|
||||
enter_editmode=False,
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
slicer_obj = bpy.context.active_object
|
||||
slicer_obj.name = "Slicer"
|
||||
slicer_obj.data.name = "Slicer"
|
||||
|
||||
slicer_obj.visible_camera = False
|
||||
slicer_obj.visible_diffuse = False
|
||||
slicer_obj.visible_glossy = False
|
||||
slicer_obj.visible_transmission = False
|
||||
slicer_obj.visible_volume_scatter = False
|
||||
slicer_obj.visible_shadow = False
|
||||
slicer_obj.hide_render = True
|
||||
slicer_obj.display_type = 'WIRE'
|
||||
slicer_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
slice_node = add_node_to_graph("Slice",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
slice_node.inputs[1].default_value = slicer_obj
|
||||
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(slice_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
slice_node.inputs[0])
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(slice_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddLocator(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_locator"
|
||||
bl_label = "Add a Locator"
|
||||
bl_description = "Add a locator to current container"
|
||||
bl_icon = "EMPTY_AXIS"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.ops.object.empty_add(type='ARROWS',
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
locator_obj = bpy.context.active_object
|
||||
locator_obj.name = "Locator"
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
parent_node = add_node_to_graph("TransformParent",
|
||||
node_group,
|
||||
get_use_link())
|
||||
|
||||
parent_node.inputs[1].default_value = locator_obj
|
||||
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(parent_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
parent_node.inputs[0])
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(parent_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ContainerProps(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.container_props"
|
||||
bl_label = "Change Container Properties"
|
||||
bl_description = "Change current ontainer properties"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
scene_scale: bpy.props.FloatProperty(name="Scene Scale",
|
||||
@@ -348,6 +445,11 @@ class ScaleContainer(bpy.types.Operator):
|
||||
min=1e-6, max=1e6,
|
||||
default=0.01) # type: ignore
|
||||
|
||||
step_size: bpy.props.FloatProperty(name="Step Size",
|
||||
soft_min=0.1, soft_max=100.0,
|
||||
min=0.1, max=1e2,
|
||||
default=1) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
@@ -358,10 +460,12 @@ class ScaleContainer(bpy.types.Operator):
|
||||
container_obj.scale[0] = self.scene_scale
|
||||
container_obj.scale[1] = self.scene_scale
|
||||
container_obj.scale[2] = self.scene_scale
|
||||
container_obj["scene_scale"] = self.scene_scale
|
||||
container_obj["step_size"] = self.step_size
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
bioxel_size = get_layer_prop_value(layer_obj, "bioxel_size")
|
||||
layer_obj.data.render.step_size = self.scene_scale * bioxel_size
|
||||
layer_obj.data.render.space = 'WORLD'
|
||||
layer_obj.data.render.step_size = self.scene_scale * self.step_size
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -371,49 +475,51 @@ class ScaleContainer(bpy.types.Operator):
|
||||
if container_obj is None:
|
||||
return self.execute(context)
|
||||
else:
|
||||
self.scene_scale = container_obj.scale[0]
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
self.scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
self.step_size = container_obj.get("step_size") or 1
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class SaveAllLayerCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_all_layer_caches"
|
||||
bl_label = "Save All Layer Caches"
|
||||
bl_description = "Save Container's caches to directory."
|
||||
def get_container_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
container_obj = context.object
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
return [obj for obj in layer_objs if layer_filter(obj, context)]
|
||||
|
||||
|
||||
class SaveContainerLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_container_layers_cache"
|
||||
bl_label = "Save Container Layers' Cache"
|
||||
bl_description = "Save all current container layers' cache to directory."
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
success_msg = "Successfully saved all container layers."
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_not_missing)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
fails = []
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
try:
|
||||
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
|
||||
layer_obj_name=layer_obj.name,
|
||||
cache_dir=self.cache_dir)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
class RemoveContainerMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_container_missing_layers"
|
||||
bl_label = "Remove Container Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
success_msg = "Successfully removed all container missing layers."
|
||||
|
||||
return {'FINISHED'}
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj, context):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_missing)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -7,9 +7,8 @@ from pathlib import Path
|
||||
|
||||
from ..exceptions import CancelledByUser
|
||||
from ..props import BIOXELNODES_Series
|
||||
from ..bioxelutils.utils import (get_all_layer_objs, get_container_obj,
|
||||
get_layer_obj,
|
||||
get_container_objs_from_selection)
|
||||
from ..bioxelutils.common import (get_all_layer_objs, get_container_obj,
|
||||
get_layer_obj, is_incompatible)
|
||||
from ..bioxelutils.container import (Container,
|
||||
add_layers,
|
||||
container_to_obj)
|
||||
@@ -17,8 +16,9 @@ from ..bioxel.layer import Layer
|
||||
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
|
||||
get_ext, parse_volumetric_data)
|
||||
|
||||
from .utils import (get_cache_dir,
|
||||
progress_update, progress_bar, select_object)
|
||||
from ..utils import (get_cache_dir,
|
||||
progress_update, progress_bar,
|
||||
select_object)
|
||||
|
||||
# 3rd-party
|
||||
import SimpleITK as sitk
|
||||
@@ -57,7 +57,7 @@ class ImportVolumetricData():
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
|
||||
read_as = "scalar"
|
||||
read_as = "SCALAR"
|
||||
|
||||
def execute(self, context):
|
||||
data_path = Path(self.filepath).resolve()
|
||||
@@ -83,7 +83,7 @@ class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_label = "Import as Scalar"
|
||||
bl_description = "Import Volumetric Data to Container as Scalar"
|
||||
bl_icon = "EVENT_S"
|
||||
read_as = "scalar"
|
||||
read_as = "SCALAR"
|
||||
|
||||
|
||||
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
|
||||
@@ -91,7 +91,7 @@ class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_label = "Import as Label"
|
||||
bl_description = "Import Volumetric Data to Container as Label"
|
||||
bl_icon = "EVENT_L"
|
||||
read_as = "label"
|
||||
read_as = "LABEL"
|
||||
|
||||
|
||||
class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
|
||||
@@ -99,7 +99,7 @@ class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_label = "Import as Label"
|
||||
bl_description = "Import Volumetric Data to Container as Label"
|
||||
bl_icon = "EVENT_C"
|
||||
read_as = "color"
|
||||
read_as = "COLOR"
|
||||
|
||||
|
||||
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
||||
@@ -142,6 +142,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
meta = None
|
||||
label_count = 0
|
||||
dtype = None
|
||||
thread = None
|
||||
_timer = None
|
||||
|
||||
@@ -160,10 +162,10 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", ""),
|
||||
("color", "Color", "")]) # type: ignore
|
||||
default="SCALAR",
|
||||
items=[("SCALAR", "Scalar", ""),
|
||||
("LABEL", "Labels", ""),
|
||||
("COLOR", "Color", "")]) # type: ignore
|
||||
|
||||
series_id: bpy.props.EnumProperty(name="Select Series",
|
||||
items=get_series_ids) # type: ignore
|
||||
@@ -172,6 +174,11 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
type=BIOXELNODES_Series) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if is_incompatible():
|
||||
self.report({"ERROR"},
|
||||
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not self.filepath:
|
||||
self.report({"WARNING"}, "No file selected.")
|
||||
return {'CANCELLED'}
|
||||
@@ -205,6 +212,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return
|
||||
|
||||
self.meta = meta
|
||||
self.label_count = int(np.max(data))
|
||||
self.dtype = data.dtype
|
||||
|
||||
# Init cancel flag
|
||||
self.is_cancelled = False
|
||||
@@ -270,26 +279,49 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
for key, value in self.meta.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
if self.read_as == "LABEL":
|
||||
if self.label_count > 100 or self.dtype.kind not in ["i", "u"]:
|
||||
self.report({"ERROR"}, "Invaild label data.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
orig_shape = self.meta['xyz_shape']
|
||||
orig_spacing = self.meta['spacing']
|
||||
if orig_spacing[2] == 1 and orig_spacing[0] < 0.1:
|
||||
spacing_log10 = math.floor(math.log10(min(*orig_spacing)))
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -spacing_log10-1),
|
||||
orig_spacing[1] * math.pow(10, -spacing_log10-1),
|
||||
|
||||
min_log10 = math.floor(math.log10(min(*orig_spacing)))
|
||||
max_log10 = math.floor(math.log10(max(*orig_spacing)))
|
||||
# min_space = min(*orig_spacing)
|
||||
# max_space = max(*orig_spacing)
|
||||
|
||||
if orig_spacing[2] == 1 and min_log10 < -1:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-2),
|
||||
orig_spacing[1] * math.pow(10, -min_log10-2),
|
||||
1)
|
||||
elif min_log10 > 0:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-1),
|
||||
orig_spacing[1] * math.pow(10, -min_log10-1),
|
||||
orig_spacing[2] * math.pow(10, -min_log10-1))
|
||||
elif max_log10 < 0:
|
||||
orig_spacing = (orig_spacing[0] * math.pow(10, -max_log10-1),
|
||||
orig_spacing[1] * math.pow(10, -max_log10-1),
|
||||
orig_spacing[2] * math.pow(10, -max_log10-1))
|
||||
|
||||
min_size = min(orig_spacing[0],
|
||||
orig_spacing[1], orig_spacing[2])
|
||||
bioxel_size = max(min_size, 1.0)
|
||||
bioxel_size = max(min(*orig_spacing), 1.0)
|
||||
|
||||
# layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
|
||||
# layer_size = get_layer_size(layer_shape,
|
||||
# bioxel_size)
|
||||
# log10 = math.floor(math.log10(max(*layer_size)))
|
||||
# log10 = max(1, log10)
|
||||
# log10 = min(3, log10)
|
||||
# scene_scale = math.pow(10, -log10)
|
||||
scene_scale = 0.01
|
||||
layer_shape = get_layer_shape(bioxel_size,
|
||||
orig_shape,
|
||||
orig_spacing)
|
||||
layer_size = get_layer_size(layer_shape,
|
||||
bioxel_size,
|
||||
0.01)
|
||||
min_log10 = math.floor(math.log10(min(*layer_size)))
|
||||
max_log10 = math.floor(math.log10(max(*layer_size)))
|
||||
|
||||
if min_log10 > 0:
|
||||
scene_scale = math.pow(10, -min_log10-2)
|
||||
elif max_log10 < 0:
|
||||
scene_scale = math.pow(10, -max_log10-2)
|
||||
else:
|
||||
scene_scale = 0.01
|
||||
|
||||
if context.area.type == "NODE_EDITOR":
|
||||
container_obj = context.object
|
||||
@@ -313,6 +345,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
channel_count=self.meta['channel_count'],
|
||||
container_obj_name=container_obj_name,
|
||||
read_as=self.read_as,
|
||||
label_count=self.label_count,
|
||||
scene_scale=scene_scale
|
||||
)
|
||||
|
||||
@@ -320,6 +353,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
# why not report in execute?
|
||||
# If this operator is executing, a new execute will when pre-one done.
|
||||
if context.window_manager.bioxelnodes_progress_factor < 1:
|
||||
print("A process is executing, please wait for it to finish.")
|
||||
return {'CANCELLED'}
|
||||
@@ -420,17 +455,17 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
layout.prop(self, "series_id")
|
||||
|
||||
|
||||
def get_sequence_sources(self, context):
|
||||
items = [("-1", "None (1 frame)", "")]
|
||||
def get_frame_sources(self, context):
|
||||
items = [("-1", "First Frame (1 frame)", "")]
|
||||
orig_shape = tuple(self.orig_shape)
|
||||
if self.frame_count > 1:
|
||||
items.append(("0", f"Frame ({self.frame_count} frames)", ""))
|
||||
items.append(("0", f"Frames ({self.frame_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count > 1:
|
||||
items.append(("4", f"Channel ({self.channel_count} frames)", ""))
|
||||
items.append(("4", f"Channels ({self.channel_count} frames)", ""))
|
||||
elif self.frame_count == 1 and self.channel_count == 1:
|
||||
items.append(("1", f"X ({orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y ({orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z ({orig_shape[2]} frames)", ""))
|
||||
items.append(("1", f"X-axis ({orig_shape[0]} frames)", ""))
|
||||
items.append(("2", f"Y-axis ({orig_shape[1]} frames)", ""))
|
||||
items.append(("3", f"Z-axis ({orig_shape[2]} frames)", ""))
|
||||
|
||||
return items
|
||||
|
||||
@@ -460,11 +495,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
channel_count: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
label_count: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", ""),
|
||||
("color", "Color", "")]) # type: ignore
|
||||
default="SCALAR",
|
||||
items=[("SCALAR", "Scalar", ""),
|
||||
("LABEL", "Labels", ""),
|
||||
("COLOR", "Color", "")]) # type: ignore
|
||||
|
||||
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
|
||||
soft_min=0.1, soft_max=10.0,
|
||||
@@ -482,11 +519,14 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
min=1e-6, max=1e6,
|
||||
default=0.01) # type: ignore
|
||||
|
||||
split_channels: bpy.props.BoolProperty(name="Split Channels",
|
||||
default=False) # type: ignore
|
||||
remap: bpy.props.BoolProperty(name="Remap to 0~1",
|
||||
default=False) # type: ignore
|
||||
|
||||
sequence_source: bpy.props.EnumProperty(name="Time Sequence From",
|
||||
items=get_sequence_sources) # type: ignore
|
||||
split_channel: bpy.props.BoolProperty(name="Split Channels",
|
||||
default=False) # type: ignore
|
||||
|
||||
frame_source: bpy.props.EnumProperty(name="Frame From",
|
||||
items=get_frame_sources) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
def import_volumetric_data_func(self, context, cancel):
|
||||
@@ -516,26 +556,26 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size)
|
||||
affine = np.dot(meta['affine'], mat_scale)
|
||||
kind = self.read_as
|
||||
kind = self.read_as.lower()
|
||||
|
||||
if cancel():
|
||||
return
|
||||
|
||||
# change shape as sequence or not
|
||||
if self.sequence_source == "-1":
|
||||
if self.frame_source == "-1":
|
||||
data = data[0:1, :, :, :, :]
|
||||
elif self.sequence_source == "0":
|
||||
elif self.frame_source == "0":
|
||||
# frame as frame
|
||||
pass
|
||||
elif self.sequence_source == "1":
|
||||
elif self.frame_source == "1":
|
||||
# X as frame
|
||||
data = data.transpose(1, 0, 2, 3, 4)
|
||||
shape = (1, shape[1], shape[2])
|
||||
elif self.sequence_source == "2":
|
||||
elif self.frame_source == "2":
|
||||
# Y as frame
|
||||
data = data.transpose(2, 1, 0, 3, 4)
|
||||
shape = (shape[0], 1, shape[2])
|
||||
elif self.sequence_source == "3":
|
||||
elif self.frame_source == "3":
|
||||
# Z as frame
|
||||
data = data.transpose(3, 1, 2, 0, 4)
|
||||
shape = (shape[0], shape[1], 1)
|
||||
@@ -551,6 +591,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
sub_progress = progress + frame * sub_progress_step
|
||||
progress_update(context, sub_progress,
|
||||
f"Processing {layer_name} Frame {frame+1}...")
|
||||
print(f"Processing {layer_name} Frame {frame+1}...")
|
||||
return progress_callback
|
||||
|
||||
layers = []
|
||||
@@ -590,11 +631,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
return
|
||||
|
||||
if kind == "color":
|
||||
|
||||
if np.issubdtype(np.uint8, data.dtype):
|
||||
data = np.multiply(data, 1.0 / 256,
|
||||
dtype=np.float32)
|
||||
elif data.dtype.kind in ['u', 'i']:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
min_val = data.min()
|
||||
max_val = data.max()
|
||||
# Avoid division by zero if all values are the same
|
||||
@@ -605,8 +648,6 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
# If all values are the same, the normalized array will be all zeros
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
else:
|
||||
data = data.astype(np.float32)
|
||||
|
||||
@@ -650,7 +691,21 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
elif kind == "scalar":
|
||||
name = self.layer_name or "Scalar"
|
||||
|
||||
if self.split_channels:
|
||||
if self.remap:
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
|
||||
min_val = data.min()
|
||||
max_val = data.max()
|
||||
# Avoid division by zero if all values are the same
|
||||
if max_val != min_val:
|
||||
# Normalize the array to the range (0,1)
|
||||
data = (data - min_val) / (max_val - min_val)
|
||||
else:
|
||||
# If all values are the same, the normalized array will be all zeros
|
||||
data = np.zeros_like(data, dtype=np.float32)
|
||||
|
||||
if self.split_channel:
|
||||
progress_step = 0.7/self.channel_count
|
||||
|
||||
for i in range(self.channel_count):
|
||||
@@ -767,15 +822,17 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
container_obj = add_layers(self.layers,
|
||||
container_obj=container_obj,
|
||||
cache_dir=get_cache_dir(context))
|
||||
cache_dir=get_cache_dir())
|
||||
else:
|
||||
name = self.container_name or "Container"
|
||||
container = Container(name=name,
|
||||
layers=self.layers)
|
||||
|
||||
step_size = container.layers[0].bioxel_size[0]*5
|
||||
container_obj = container_to_obj(container,
|
||||
scene_scale=self.scene_scale,
|
||||
cache_dir=get_cache_dir(context))
|
||||
step_size=step_size,
|
||||
cache_dir=get_cache_dir())
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
@@ -806,54 +863,69 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
# change shape as sequence or not
|
||||
channel_count = self.channel_count
|
||||
if self.sequence_source == "-1":
|
||||
frame_count = self.frame_count
|
||||
if self.frame_source == "-1":
|
||||
frame_count = 1
|
||||
pass
|
||||
elif self.sequence_source == "0":
|
||||
elif self.frame_source == "0":
|
||||
# frame as frame
|
||||
pass
|
||||
elif self.sequence_source == "1":
|
||||
elif self.frame_source == "1":
|
||||
frame_count = orig_shape[0]
|
||||
layer_shape = (1, layer_shape[1], layer_shape[2])
|
||||
elif self.sequence_source == "2":
|
||||
elif self.frame_source == "2":
|
||||
frame_count = orig_shape[1]
|
||||
layer_shape = (layer_shape[0], 1, layer_shape[2])
|
||||
elif self.sequence_source == "3":
|
||||
elif self.frame_source == "3":
|
||||
frame_count = orig_shape[2]
|
||||
layer_shape = (layer_shape[0], layer_shape[1], 1)
|
||||
else:
|
||||
channel_count = 1
|
||||
|
||||
import_channel = channel_count if self.split_channels or channel_count == 1 else "combined"
|
||||
if self.read_as == "SCALAR":
|
||||
layer_count = channel_count if self.split_channel else 1
|
||||
channel_count = 1
|
||||
elif self.read_as == "LABEL":
|
||||
layer_count = self.label_count
|
||||
channel_count = 1
|
||||
else:
|
||||
layer_count = 1
|
||||
channel_count = 3
|
||||
|
||||
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
||||
layer_shape_text = f"Shape from {str(orig_shape)} to {str(layer_shape)}"
|
||||
orig_shape_text = f"[{self.frame_count}, ({orig_shape[0]},{orig_shape[1]},{orig_shape[2]}), {self.channel_count}]"
|
||||
layer_shape_text = f"{layer_count} x [{frame_count}, ({layer_shape[0]},{layer_shape[1]},{layer_shape[2]}), {channel_count}]"
|
||||
|
||||
if bioxel_count > 100000000:
|
||||
layer_shape_text += "**TOO LARGE!**"
|
||||
|
||||
layer_size = get_layer_size(layer_shape,
|
||||
self.bioxel_size,
|
||||
self.scene_scale)
|
||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||
|
||||
layout = self.layout
|
||||
if self.container_obj_name == "":
|
||||
layout.prop(self, "container_name")
|
||||
layout.prop(self, "layer_name")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "container_name")
|
||||
panel.prop(self, "scene_scale")
|
||||
panel.label(text=layer_size_text)
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "layer_name")
|
||||
panel.prop(self, "bioxel_size")
|
||||
row = panel.row()
|
||||
row.prop(self, "orig_spacing")
|
||||
panel.label(text=layer_shape_text)
|
||||
panel.prop(self, "frame_source")
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "sequence_source")
|
||||
if self.read_as == "SCALAR":
|
||||
panel.prop(self, "split_channel",
|
||||
text=f"Split Channel as Multi Layer")
|
||||
|
||||
if self.read_as == "scalar":
|
||||
panel = layout.box()
|
||||
panel.prop(self, "split_channels",
|
||||
text=f"Split Channels (Get {channel_count} channels, import {import_channel} channels)")
|
||||
|
||||
if self.container_obj_name == "":
|
||||
layer_size = get_layer_size(layer_shape,
|
||||
self.bioxel_size,
|
||||
self.scene_scale)
|
||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||
panel = layout.box()
|
||||
panel.prop(self, "scene_scale")
|
||||
panel.label(text=layer_size_text)
|
||||
panel.label(
|
||||
text="Dimension Order: [Frame, (X-axis,Y-axis,Z-axis), Channel]")
|
||||
panel.label(
|
||||
text=f"Shape from {orig_shape_text} to {layer_shape_text}")
|
||||
|
||||
|
||||
class ExportVolumetricData(bpy.types.Operator):
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
from ..exceptions import NoContent
|
||||
from ..bioxel.layer import Layer
|
||||
from ..utils import copy_to_dir
|
||||
from ..customnodes.nodes import AddCustomNode
|
||||
from ..bioxelutils.utils import (get_container_obj,
|
||||
get_layer_prop_value,
|
||||
get_container_layer_objs,
|
||||
get_node_type, set_layer_prop_value)
|
||||
from ..bioxelutils.common import (get_container_obj, get_layer_kind, get_layer_label, get_layer_name,
|
||||
get_layer_prop_value,
|
||||
get_container_layer_objs,
|
||||
get_node_type, is_missing_layer, set_layer_prop_value)
|
||||
from ..bioxelutils.layer import layer_to_obj, obj_to_layer
|
||||
from .utils import get_cache_dir, get_layer_item_label, get_layer_label, get_preferences
|
||||
from ..nodes import custom_nodes
|
||||
from ..utils import get_cache_dir, copy_to_dir
|
||||
|
||||
|
||||
def get_label_layer_selection(self, context):
|
||||
@@ -21,134 +18,244 @@ def get_label_layer_selection(self, context):
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
if get_layer_prop_value(layer_obj, "kind") == "label":
|
||||
kind = get_layer_prop_value(layer_obj, "kind")
|
||||
name = get_layer_prop_value(layer_obj, "name")
|
||||
if kind == "label":
|
||||
items.append((layer_obj.name,
|
||||
layer_obj.name,
|
||||
name,
|
||||
""))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def get_selected_objs_in_node_tree(context):
|
||||
select_objs = []
|
||||
# node_group = context.space_data.edit_tree
|
||||
for node in context.selected_nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
layer_obj = node.inputs[0].default_value
|
||||
if layer_obj != None:
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
if cache_filepath.is_file():
|
||||
select_objs.append(layer_obj)
|
||||
return select_objs
|
||||
|
||||
|
||||
class LayerOperator():
|
||||
class FetchLayer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fetch_layer"
|
||||
bl_label = "Fetch Layer"
|
||||
bl_description = "Fetch layer from current container"
|
||||
bl_icon = "NODE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
layer_obj_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN"}) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
layer_obj_name = properties.layer_obj_name
|
||||
layer_obj = bpy.data.objects.get(layer_obj_name)
|
||||
return "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
|
||||
for prop in ["kind",
|
||||
"bioxel_size",
|
||||
"shape",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
@property
|
||||
def layer_obj(self):
|
||||
return bpy.data.objects.get(self.layer_obj_name)
|
||||
|
||||
@property
|
||||
def is_lost(self):
|
||||
if self.layer_obj is None:
|
||||
return None
|
||||
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
self.layer_obj.data.filepath)).resolve()
|
||||
return not cache_filepath.is_file()
|
||||
|
||||
|
||||
class FetchLayer(bpy.types.Operator, LayerOperator, AddCustomNode):
|
||||
bl_idname = "bioxelnodes.fetch_layer"
|
||||
bl_label = "Fetch Layer"
|
||||
bl_description = "Fetch Layer"
|
||||
bl_icon = "NODE"
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.is_lost:
|
||||
if is_missing_layer(self.layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.get_node_tree(self.node_type, self.node_link)
|
||||
prev_context = bpy.context.area.type
|
||||
bpy.context.area.type = 'NODE_EDITOR'
|
||||
bpy.ops.node.add_node('INVOKE_DEFAULT',
|
||||
type='GeometryNodeGroup',
|
||||
use_transform=True)
|
||||
bpy.context.area.type = prev_context
|
||||
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||
node_name="FetchLayer",
|
||||
node_label="Fetch Layer")
|
||||
node = bpy.context.active_node
|
||||
|
||||
self.assign_node_tree(node)
|
||||
node.show_options = False
|
||||
layer_obj = self.layer_obj
|
||||
node.inputs[0].default_value = layer_obj
|
||||
node.label = get_layer_label(layer_obj)
|
||||
node.inputs[0].default_value = self.layer_obj
|
||||
node.label = get_layer_label(self.layer_obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.nodes_file = custom_nodes.nodes_file
|
||||
self.node_type = "BioxelNodes_FetchLayer"
|
||||
return self.execute(context)
|
||||
|
||||
def get_selected_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
select_objs = []
|
||||
# node_group = context.space_data.edit_tree
|
||||
for node in context.selected_nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
layer_obj = node.inputs[0].default_value
|
||||
if layer_obj is not None:
|
||||
if layer_filter(layer_obj, context):
|
||||
select_objs.append(layer_obj)
|
||||
|
||||
return list(set(select_objs))
|
||||
|
||||
|
||||
class FetchLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_ADD_LAYER"
|
||||
bl_label = "Fetch Layer"
|
||||
|
||||
def draw(self, context):
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
layout = self.layout
|
||||
|
||||
for layer_obj in layer_objs:
|
||||
op = layout.operator(FetchLayer.bl_idname,
|
||||
text=get_layer_item_label(context, layer_obj))
|
||||
op.layer_obj_name = layer_obj.name
|
||||
def get_selected_layer(context, layer_filter=None):
|
||||
layer_objs = get_selected_layers(context, layer_filter)
|
||||
return layer_objs[0] if len(layer_objs) > 0 else None
|
||||
|
||||
|
||||
class ModifyLayerOperator(LayerOperator):
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
class OutputLayerOperator():
|
||||
new_layer_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
"""do the operation"""
|
||||
return orig_layer
|
||||
|
||||
def add_layer_node(self, context, layer):
|
||||
layer_obj = layer_to_obj(layer,
|
||||
container_obj=self.layer_obj.parent,
|
||||
cache_dir=get_cache_dir(context))
|
||||
container_obj=context.object,
|
||||
cache_dir=get_cache_dir())
|
||||
|
||||
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
|
||||
layer_obj_name=layer_obj.name)
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.is_lost:
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
orig_layer = obj_to_layer(self.layer_obj)
|
||||
new_layer = self.layer_operate(orig_layer)
|
||||
orig_layer = obj_to_layer(layer_obj)
|
||||
try:
|
||||
new_layer = self.operate(orig_layer, context)
|
||||
except NoContent as e:
|
||||
self.report({"WARNING"}, e.message)
|
||||
return {'FINISHED'}
|
||||
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
bl_idname = "bioxelnodes.resample_scalar"
|
||||
bl_label = "Resample Scalar"
|
||||
bl_description = "Resample Scalar"
|
||||
class LayerOperator():
|
||||
|
||||
def operate(self, layer_obj: bpy.types.Object, context):
|
||||
"""do the operation"""
|
||||
...
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.operate(layer_obj, context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RetimeLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.retime_layer"
|
||||
bl_label = "Retime Sequence"
|
||||
bl_description = "Retime layer time sequence"
|
||||
bl_icon = "TIME"
|
||||
|
||||
frame_duration: bpy.props.IntProperty(name="Frames") # type: ignore
|
||||
frame_start: bpy.props.IntProperty(name="Start") # type: ignore
|
||||
frame_offset: bpy.props.IntProperty(name="Offset") # type: ignore
|
||||
sequence_mode: bpy.props.EnumProperty(name="Mode",
|
||||
default="REPEAT",
|
||||
items=[("CLIP", "Clip", ""),
|
||||
("EXTEND", "Extend", ""),
|
||||
("REPEAT", "Repeat", ""),
|
||||
("PING_PONG", "Ping-Pong", "")]) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.frame_duration = self.frame_duration
|
||||
layer_obj.data.frame_start = self.frame_start
|
||||
layer_obj.data.frame_offset = self.frame_offset
|
||||
layer_obj.data.sequence_mode = self.sequence_mode
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.frame_duration = layer_obj.data.frame_duration
|
||||
self.frame_start = layer_obj.data.frame_start
|
||||
self.frame_offset = layer_obj.data.frame_offset
|
||||
self.sequence_mode = layer_obj.data.sequence_mode
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Retime {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class RelocateLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.relocate_layer"
|
||||
bl_label = "Relocate Layer Cache"
|
||||
bl_description = "Relocate layer cache"
|
||||
bl_icon = "FILE"
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.filepath = self.filepath
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class RenameLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.rename_layer"
|
||||
bl_label = "Rename Layer"
|
||||
bl_description = "Rename layer"
|
||||
bl_icon = "FILE_FONT"
|
||||
|
||||
new_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
name = f"{layer_obj.parent.name}_{self.new_name}"
|
||||
layer_obj.name = name
|
||||
layer_obj.data.name = name
|
||||
|
||||
set_layer_prop_value(layer_obj, "name", self.new_name)
|
||||
|
||||
node_group = context.space_data.edit_tree
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node.label = self.new_name
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.new_name = get_layer_name(layer_obj)
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Rename {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class ResampleLayer(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.resample_layer"
|
||||
bl_label = "Resample Value"
|
||||
bl_description = "Resample value"
|
||||
bl_icon = "ALIASED"
|
||||
|
||||
smooth: bpy.props.IntProperty(name="Smooth Iteration",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
bioxel_size: bpy.props.FloatProperty(
|
||||
name="Bioxel Size",
|
||||
soft_min=0.1, soft_max=10.0,
|
||||
@@ -161,19 +268,21 @@ class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
int(orig_shape[1]*orig_size/new_size),
|
||||
int(orig_shape[2]*orig_size/new_size))
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
modified_layer = orig_layer.copy()
|
||||
new_shape = self.get_new_shape(modified_layer.shape,
|
||||
modified_layer.bioxel_size[0],
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_shape = self.get_new_shape(new_layer.shape,
|
||||
new_layer.bioxel_size[0],
|
||||
self.bioxel_size)
|
||||
|
||||
modified_layer.resize(new_shape)
|
||||
modified_layer.name = f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
|
||||
return modified_layer
|
||||
new_layer.resize(new_shape, self.smooth)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
|
||||
return new_layer
|
||||
|
||||
def draw(self, context):
|
||||
orig_shape = get_layer_prop_value(self.layer_obj, "shape")
|
||||
orig_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
|
||||
layer_obj = get_selected_layer(context)
|
||||
orig_shape = get_layer_prop_value(layer_obj, "shape")
|
||||
orig_size = get_layer_prop_value(layer_obj, "bioxel_size")
|
||||
new_shape = self.get_new_shape(orig_shape,
|
||||
orig_size,
|
||||
self.bioxel_size)
|
||||
@@ -186,45 +295,49 @@ class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
layer_shape_text += "**TOO LARGE!**"
|
||||
|
||||
layout = self.layout
|
||||
layout.prop(self, "new_layer_name")
|
||||
layout.prop(self, "bioxel_size")
|
||||
layout.prop(self, "smooth")
|
||||
layout.label(text=layer_shape_text)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
bioxel_size = get_layer_prop_value(self.layer_obj, "bioxel_size")
|
||||
|
||||
self.bioxel_size = bioxel_size
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
self.bioxel_size = get_layer_prop_value(layer_obj,
|
||||
"bioxel_size")
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title=f"Resample {self.layer_obj.name}")
|
||||
title=f"Resample {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class SignScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
class SignScalar(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Scalar"
|
||||
bl_description = "Sign the scalar value"
|
||||
bl_label = "Sign Value"
|
||||
bl_description = "Sign value"
|
||||
bl_icon = "REMOVE"
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.data = -orig_layer.data
|
||||
modified_layer.name = f"{orig_layer.name}_Sign"
|
||||
return modified_layer
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.data = -orig_layer.data
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_Sign"
|
||||
return new_layer
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to sign {self.layer_obj.name}?")
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Sign {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FillOperator(ModifyLayerOperator):
|
||||
class FillOperator(OutputLayerOperator):
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
@@ -238,13 +351,13 @@ class FillOperator(ModifyLayerOperator):
|
||||
) # type: ignore
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
scalar_min = get_layer_prop_value(self.layer_obj, "min")
|
||||
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
scalar_min = get_layer_prop_value(layer_obj, "min")
|
||||
self.fill_value = min(scalar_min, 0)
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title=f"Fill {self.layer_obj.name}")
|
||||
title=f"Fill {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
@@ -253,7 +366,7 @@ class FillOperator(ModifyLayerOperator):
|
||||
class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
||||
bl_label = "Fill Value by Threshold"
|
||||
bl_description = "Fill Value by Threshold"
|
||||
bl_description = "Fill value by threshold"
|
||||
bl_icon = "EMPTY_SINGLE_ARROW"
|
||||
|
||||
threshold: bpy.props.FloatProperty(
|
||||
@@ -262,21 +375,22 @@ class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||
default=128,
|
||||
) # type: ignore
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = data <= self.threshold \
|
||||
if self.invert else data > self.threshold
|
||||
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{orig_layer.name}_F-{self.threshold}"
|
||||
return modified_layer
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.threshold:.2f}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class FillByRange(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_range"
|
||||
bl_label = "Fill Value by Range"
|
||||
bl_description = "Fill Value by Range"
|
||||
bl_description = "Fill value by range"
|
||||
bl_icon = "IPO_CONSTANT"
|
||||
|
||||
from_min: bpy.props.FloatProperty(
|
||||
@@ -291,82 +405,112 @@ class FillByRange(bpy.types.Operator, FillOperator):
|
||||
default=256,
|
||||
) # type: ignore
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
|
||||
(data > self.from_min) & (data < self.from_max)
|
||||
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{orig_layer.name}_F-{self.from_min}-{self.from_max}"
|
||||
return modified_layer
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.from_min:.2f}-{self.from_max:.2f}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class FillByLabel(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_label"
|
||||
bl_label = "Fill Value by Label"
|
||||
bl_description = "Fill Value by Label Area"
|
||||
bl_description = "Fill value by label"
|
||||
bl_icon = "MESH_CAPSULE"
|
||||
|
||||
smooth: bpy.props.IntProperty(name="Smooth Iteration",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
|
||||
items=get_label_layer_selection) # type: ignore
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
label_obj = bpy.data.objects.get(self.label_obj_name)
|
||||
if not label_obj:
|
||||
self.report({"WARNING"}, "Cannot find any label layer.")
|
||||
return {'FINISHED'}
|
||||
if label_obj is None:
|
||||
raise NoContent("Cannot find any label layer.")
|
||||
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(orig_layer.shape)
|
||||
label_layer.resize(orig_layer.shape, self.smooth)
|
||||
mask = np.amax(label_layer.data, -1)
|
||||
if self.invert:
|
||||
mask = 1 - mask
|
||||
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{orig_layer.name}_F-{label_layer.name}"
|
||||
return modified_layer
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 3)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{label_layer.name}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class CombineLabels(bpy.types.Operator, ModifyLayerOperator):
|
||||
class CombineLabels(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.combine_labels"
|
||||
bl_label = "Combine Labels"
|
||||
bl_description = "Combine all selected labels"
|
||||
bl_icon = "MOD_BUILD"
|
||||
|
||||
def execute(self, context):
|
||||
label_objs = [obj for obj in get_selected_objs_in_node_tree(context)
|
||||
if get_layer_prop_value(obj, "kind") == "label"]
|
||||
def layer_filter(layer_obj, context):
|
||||
return get_layer_kind(layer_obj) == "label"
|
||||
|
||||
label_objs = get_selected_layers(context, layer_filter)
|
||||
|
||||
if len(label_objs) < 2:
|
||||
self.report({"WARNING"}, "Not enough layers.")
|
||||
return {'FINISHED'}
|
||||
|
||||
base_obj = label_objs[0]
|
||||
label_objs = label_objs[1:]
|
||||
|
||||
base_layer = obj_to_layer(base_obj)
|
||||
modified_layer = base_layer.copy()
|
||||
new_layer = base_layer.copy()
|
||||
label_names = [base_layer.name]
|
||||
|
||||
for label_obj in label_objs:
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(base_layer.shape)
|
||||
modified_layer.data = np.maximum(
|
||||
modified_layer.data, label_layer.data)
|
||||
new_layer.data = np.maximum(
|
||||
new_layer.data, label_layer.data)
|
||||
label_names.append(label_layer.name)
|
||||
|
||||
modified_layer.name = f"C-{'-'.join(label_names)}"
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"C-{'-'.join(label_names)}"
|
||||
|
||||
self.add_layer_node(context, modified_layer)
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveLayerCache(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.save_layer_cache"
|
||||
bl_label = "Save Layer Cache"
|
||||
bl_description = "Save Layer Cache"
|
||||
bl_icon = "FILE_TICK"
|
||||
class BatchLayerOperator():
|
||||
success_msg = "Successfully done selected layers."
|
||||
fail_msg = "fails."
|
||||
|
||||
def execute(self, context):
|
||||
layer_objs = self.get_layers(context)
|
||||
|
||||
fails = []
|
||||
for layer_obj in layer_objs:
|
||||
try:
|
||||
self.operate(layer_obj, context)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, self.success_msg)
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} {self.fail_msg}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveLayersCache(BatchLayerOperator):
|
||||
fail_msg = "fail to save."
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
@@ -374,23 +518,14 @@ class SaveLayerCache(bpy.types.Operator, LayerOperator):
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.is_lost:
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
# "//"
|
||||
def operate(self, layer_obj, context):
|
||||
output_dir = bpy.path.abspath(self.cache_dir)
|
||||
source_dir = bpy.path.abspath(self.layer_obj.data.filepath)
|
||||
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = self.layer_obj.data.is_sequence
|
||||
is_sequence = layer_obj.data.is_sequence
|
||||
|
||||
name = self.layer_obj.name if is_sequence else f"{self.layer_obj.name}.vdb"
|
||||
name = layer_obj.name if is_sequence else f"{layer_obj.name}.vdb"
|
||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
||||
if is_sequence else Path(output_dir, name).resolve()
|
||||
|
||||
@@ -401,124 +536,69 @@ class SaveLayerCache(bpy.types.Operator, LayerOperator):
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
self.layer_obj.data.filepath = bpy.path.relpath(str(output_path),
|
||||
start=str(blend_path))
|
||||
layer_obj.data.filepath = bpy.path.relpath(str(output_path),
|
||||
start=str(blend_path))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class RenameLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.rename_layer"
|
||||
bl_label = "Rename Layer"
|
||||
bl_description = "Rename Layer"
|
||||
bl_icon = "FILE_FONT"
|
||||
class RemoveLayers(BatchLayerOperator):
|
||||
fail_msg = "fail to remove."
|
||||
|
||||
name: bpy.props.StringProperty(name="New Name") # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if self.is_lost:
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
name = f"{self.layer_obj.parent.name}_{self.name}"
|
||||
self.layer_obj.name = name
|
||||
self.layer_obj_name = name
|
||||
self.layer_obj.data.name = name
|
||||
|
||||
set_layer_prop_value(self.layer_obj, "name", self.name)
|
||||
|
||||
node_group = context.space_data.edit_tree
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == self.layer_obj:
|
||||
node.label = self.name
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
self.name = get_layer_prop_value(self.layer_obj, "name")
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500,
|
||||
title=f"rename {self.layer_obj.name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class RemoveLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.remove_layer"
|
||||
bl_label = "Remove Selected Layer"
|
||||
bl_description = "Remove Layer"
|
||||
bl_icon = "TRASH"
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
node_group = context.space_data.edit_tree
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == self.layer_obj:
|
||||
node_group.nodes.remove(node)
|
||||
def operate(self, layer_obj, context):
|
||||
for node_group in bpy.data.node_groups:
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node_group.nodes.remove(node)
|
||||
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
self.layer_obj.data.filepath)).resolve()
|
||||
layer_obj.data.filepath)).resolve()
|
||||
|
||||
if cache_filepath.is_file():
|
||||
if self.layer_obj.data.is_sequence:
|
||||
if layer_obj.data.is_sequence:
|
||||
for f in cache_filepath.parent.iterdir():
|
||||
f.unlink(missing_ok=True)
|
||||
else:
|
||||
cache_filepath.unlink(missing_ok=True)
|
||||
|
||||
# also remove layer object
|
||||
bpy.data.volumes.remove(self.layer_obj.data)
|
||||
bpy.data.volumes.remove(layer_obj.data)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove {self.layer_obj.name}?")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class RemoveMissingLayers(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.remove_lost_layers"
|
||||
bl_label = "Remove All Missing Layers"
|
||||
bl_description = "Remove all missing "
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = context.object
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
if cache_filepath.is_file():
|
||||
continue
|
||||
bpy.ops.bioxelnodes.remove_layer('EXEC_DEFAULT',
|
||||
layer_obj_name=layer_obj.name)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
message=f"Are you sure to remove them?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class SaveSelectedLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_selected_layers_cache"
|
||||
bl_label = "Save Selected Layers Cache"
|
||||
bl_description = "Save selected layers' Cache"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all selected layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_selected_layers(context, is_not_missing)
|
||||
|
||||
|
||||
class RemoveSelectedLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_selected_layers"
|
||||
bl_label = "Remove Selected Layers"
|
||||
bl_description = "Remove selected layers"
|
||||
bl_icon = "TRASH"
|
||||
|
||||
success_msg = "Successfully removed all selected layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
return get_selected_layers(context)
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from .utils import get_cache_dir
|
||||
from ..nodes import custom_nodes
|
||||
from ..bioxelutils.utils import (get_container_objs_from_selection,
|
||||
get_all_layer_objs,
|
||||
get_container_layer_objs)
|
||||
|
||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||
from ..bioxelutils.common import get_all_layer_objs, is_missing_layer, set_file_prop
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
class ReLinkNodes(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_nodes"
|
||||
bl_label = "Relink Nodes to Addon"
|
||||
bl_description = "Relink all nodes to addon source"
|
||||
class ReLinkNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_node_lib"
|
||||
bl_label = "Relink Node Library"
|
||||
bl_description = "Relink all nodes to addon library source"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
node_lib_filename: bpy.props.StringProperty(
|
||||
default=""
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
file_name = Path(custom_nodes.nodes_file).name
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
lib_name = lib_path.name
|
||||
if lib_name == file_name:
|
||||
if str(lib_path) != custom_nodes.nodes_file:
|
||||
lib.filepath = custom_nodes.nodes_file
|
||||
lib_filepath = Path(NODE_LIB_FILEPATH.parent,
|
||||
f"{self.node_lib_filename}.blend")
|
||||
node_libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_lib = node_group.library
|
||||
if node_lib:
|
||||
node_libs.append(node_lib)
|
||||
|
||||
node_libs = list(set(node_libs))
|
||||
|
||||
for node_lib in node_libs:
|
||||
node_lib.filepath = str(lib_filepath)
|
||||
# FIXME: may cause crash
|
||||
node_lib.reload()
|
||||
|
||||
self.report({"INFO"}, f"Successfully relinked.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveStagedData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_staged_data"
|
||||
bl_label = "Save Staged Data"
|
||||
bl_description = "Save all staged data in this file for sharing"
|
||||
|
||||
save_cache: bpy.props.BoolProperty(
|
||||
name="Save Layer Caches",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
save_lib: bpy.props.BoolProperty(
|
||||
name="Save Node Library File",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
class SaveNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_node_lib"
|
||||
bl_label = "Save Node Library"
|
||||
bl_description = "Save node library file to local"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
lib_dir: bpy.props.StringProperty(
|
||||
name="Library Directory",
|
||||
@@ -57,98 +54,66 @@ class SaveStagedData(bpy.types.Operator):
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.save_lib:
|
||||
files = []
|
||||
for classname in dir(bpy.types):
|
||||
if CLASS_PREFIX in classname:
|
||||
cls = getattr(bpy.types, classname)
|
||||
files.append(cls.nodes_file)
|
||||
files = list(set(files))
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
local_lib_path: Path = Path(lib_dir, NODE_LIB_FILEPATH.name).resolve()
|
||||
addon_lib_path: Path = NODE_LIB_FILEPATH
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
for file in files:
|
||||
file_name = Path(file).name
|
||||
# "//"
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
if local_lib_path != addon_lib_path:
|
||||
shutil.copy(addon_lib_path, local_lib_path)
|
||||
|
||||
output_path: Path = Path(lib_dir, file_name).resolve()
|
||||
source_path: Path = Path(file).resolve()
|
||||
libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
if node_group.library:
|
||||
libs.append(node_group.library)
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
libs = list(set(libs))
|
||||
for lib in libs:
|
||||
lib.filepath = bpy.path.relpath(str(local_lib_path),
|
||||
start=str(blend_path))
|
||||
|
||||
for lib in bpy.data.libraries:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
if lib_path == source_path:
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
lib.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||
|
||||
if self.save_cache:
|
||||
fails = []
|
||||
for layer_obj in get_all_layer_objs():
|
||||
try:
|
||||
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
|
||||
layer_obj_name=layer_obj.name,
|
||||
cache_dir=self.cache_dir)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} fail to save.")
|
||||
set_file_prop("addon_version", VERSION)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.data.is_saved
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_cache")
|
||||
panel.prop(self, "cache_dir")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_lib")
|
||||
panel.prop(self, "lib_dir")
|
||||
|
||||
|
||||
class CleanAllCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_all_caches"
|
||||
bl_label = "Clean All Caches in Temp"
|
||||
bl_description = "Clean all caches saved in temp"
|
||||
class CleanTemp(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_temp"
|
||||
bl_label = "Clean Temp"
|
||||
bl_description = "Clean all cache in temp (include other project cache)"
|
||||
|
||||
def execute(self, context):
|
||||
cache_dir = get_cache_dir(context)
|
||||
cache_dir = get_cache_dir()
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
self.report({"INFO"}, f"Successfully cleaned caches.")
|
||||
self.report({"INFO"}, f"Successfully cleaned temp.")
|
||||
return {'FINISHED'}
|
||||
except:
|
||||
self.report({"WARNING"},
|
||||
"Fail to clean caches, you may do it manually.")
|
||||
"Fail to clean temp, you may do it manually.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message="All caches will be cleaned, include other project files, do you still want to clean?")
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class RenderSettingPreset(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.render_setting_preset"
|
||||
bl_label = "Render Setting Preset"
|
||||
bl_description = "Render Setting Preset"
|
||||
bl_label = "Render Setting Presets"
|
||||
bl_description = "Render setting presets for bioxel"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
PRESETS = {
|
||||
"preview_e": "Preview (EEVEE)",
|
||||
@@ -190,21 +155,24 @@ class RenderSettingPreset(bpy.types.Operator):
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 1
|
||||
bpy.context.scene.cycles.volume_step_rate = 1
|
||||
# bpy.context.scene.cycles.use_fast_gi = True
|
||||
|
||||
elif self.preset == "production_c":
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.context.scene.cycles.shading_system = True
|
||||
bpy.context.scene.cycles.volume_bounces = 16
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 32
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 0.1
|
||||
bpy.context.scene.cycles.volume_step_rate = 0.1
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 0.5
|
||||
bpy.context.scene.cycles.volume_step_rate = 0.5
|
||||
# bpy.context.scene.cycles.use_fast_gi = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SliceViewer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.slice_viewer"
|
||||
bl_label = "Slice Viewer"
|
||||
bl_description = "Slice Viewer"
|
||||
bl_description = "A preview scene setting for viewing slicers"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def execute(self, context):
|
||||
@@ -215,7 +183,7 @@ class SliceViewer(bpy.types.Operator):
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = False
|
||||
|
||||
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
area.spaces[0].shading.type = 'MATERIAL'
|
||||
@@ -224,5 +192,47 @@ class SliceViewer(bpy.types.Operator):
|
||||
area.spaces[0].shading.use_scene_lights = False
|
||||
area.spaces[0].shading.use_scene_world = False
|
||||
|
||||
|
||||
return {'FINISHED'}
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_all_layers(layer_filter=None):
|
||||
def _layer_filter(layer_obj):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
layer_objs = get_all_layer_objs()
|
||||
return [obj for obj in layer_objs if layer_filter(obj)]
|
||||
|
||||
|
||||
class SaveAllLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_all_layers_cache"
|
||||
bl_label = "Save All Layers Cache"
|
||||
bl_description = "Save all cache of this file"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_not_missing)
|
||||
|
||||
|
||||
class RemoveAllMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_all_missing_layers"
|
||||
bl_label = "Remove All Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_missing)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
62
bioxelnodes/operators/node.py
Normal file
62
bioxelnodes/operators/node.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import bpy
|
||||
|
||||
from ..bioxelutils.common import get_file_prop, is_incompatible, local_lib_not_updated
|
||||
from ..constants import VERSION
|
||||
from ast import literal_eval
|
||||
from ..bioxelutils.node import assign_node_group, get_node_group
|
||||
from ..utils import get_use_link
|
||||
|
||||
|
||||
class AddNode(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_node"
|
||||
bl_label = "Add Node"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
node_name: bpy.props.StringProperty(
|
||||
default='',
|
||||
) # type: ignore
|
||||
|
||||
node_label: bpy.props.StringProperty(
|
||||
default=''
|
||||
) # type: ignore
|
||||
|
||||
node_description: bpy.props.StringProperty(
|
||||
default="",
|
||||
) # type: ignore
|
||||
|
||||
@property
|
||||
def node_type(self):
|
||||
return f"BioxelNodes_{self.node_name}"
|
||||
|
||||
def execute(self, context):
|
||||
space = context.space_data
|
||||
if space.type != "NODE_EDITOR":
|
||||
self.report({"ERROR"}, "Not in node editor.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not space.edit_tree.is_editable:
|
||||
self.report({"ERROR"}, "Not editable.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if is_incompatible():
|
||||
self.report({"ERROR"},
|
||||
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
get_node_group(self.node_type, get_use_link())
|
||||
bpy.ops.node.add_node(
|
||||
'INVOKE_DEFAULT',
|
||||
type='GeometryNodeGroup',
|
||||
use_transform=True
|
||||
)
|
||||
node = bpy.context.active_node
|
||||
assign_node_group(node, self.node_type)
|
||||
node.label = self.node_label
|
||||
|
||||
node.show_options = False
|
||||
|
||||
if local_lib_not_updated():
|
||||
self.report({"WARNING"},
|
||||
"Local library version does not match the current addon version, which may cause problems, please save the node library again.")
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -1,60 +0,0 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
from ..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}"
|
||||
@@ -1,6 +1,7 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
@@ -10,14 +11,19 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||
) # type: ignore
|
||||
|
||||
do_change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting",
|
||||
change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting on First Import",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
node_import_method: bpy.props.EnumProperty(name="Node Import Method",
|
||||
default="LINK",
|
||||
items=[("LINK", "Link", ""),
|
||||
("APPEND", "Append (Reuse Data)", "")]) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
layout.prop(self, "do_change_render_setting")
|
||||
layout.prop(self, "change_render_setting")
|
||||
layout.prop(self, "node_import_method")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import bpy
|
||||
|
||||
from .bioxelutils.utils import get_node_type
|
||||
from .bioxelutils.common import get_node_type
|
||||
|
||||
|
||||
class BIOXELNODES_UL_layer_list(bpy.types.UIList):
|
||||
@@ -27,6 +27,8 @@ def select_layer(self, context):
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node.select = True
|
||||
|
||||
layer_list_UL.layer_list_active = -1
|
||||
|
||||
|
||||
class BIOXELNODES_Layer(bpy.types.PropertyGroup):
|
||||
obj_name: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import bpy
|
||||
|
||||
|
||||
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
@@ -30,3 +31,42 @@ def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
|
||||
if not target_path.exists():
|
||||
raise Exception
|
||||
|
||||
|
||||
def select_object(target_obj):
|
||||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
target_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = target_obj
|
||||
|
||||
|
||||
def progress_bar(self, context):
|
||||
row = self.layout.row()
|
||||
row.progress(
|
||||
factor=context.window_manager.bioxelnodes_progress_factor,
|
||||
type="BAR",
|
||||
text=context.window_manager.bioxelnodes_progress_text
|
||||
)
|
||||
row.scale_x = 2
|
||||
|
||||
|
||||
def progress_update(context, factor, text=""):
|
||||
context.window_manager.bioxelnodes_progress_factor = factor
|
||||
context.window_manager.bioxelnodes_progress_text = text
|
||||
|
||||
|
||||
def get_preferences():
|
||||
return bpy.context.preferences.addons[__package__].preferences
|
||||
|
||||
|
||||
def get_cache_dir():
|
||||
preferences = get_preferences()
|
||||
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
return str(cache_path)
|
||||
|
||||
|
||||
def get_use_link():
|
||||
preferences = get_preferences()
|
||||
return preferences.node_import_method == "LINK"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "bioxelnodes"
|
||||
version = "0.4.0"
|
||||
version = "1.0.0"
|
||||
description = ""
|
||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user