fix: eevee specular too high
feat: add pie cutter feat: support 4d time data chora: change operators name docs: update README.md
This commit is contained in:
@@ -9,7 +9,7 @@ bl_info = {
|
||||
"author": "Ma Nan",
|
||||
"description": "",
|
||||
"blender": (4, 1, 0),
|
||||
"version": (0, 2, 6),
|
||||
"version": (0, 2, 7),
|
||||
"location": "File -> Import",
|
||||
"warning": "",
|
||||
"category": "Node"
|
||||
|
||||
Binary file not shown.
@@ -13,7 +13,7 @@ from . import skimage as ski
|
||||
from .nodes import custom_nodes
|
||||
from .props import BIOXELNODES_Series
|
||||
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer, get_text_index_str,
|
||||
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, show_message)
|
||||
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, save_vdbs, select_object, show_message)
|
||||
|
||||
try:
|
||||
import SimpleITK as sitk
|
||||
@@ -124,7 +124,11 @@ def parse_volume_data(filepath: str, series_id=""):
|
||||
series_files = reader.GetGDCMSeriesFileNames(
|
||||
str(dir_path), series_id)
|
||||
reader.SetFileNames(series_files)
|
||||
|
||||
itk_volume = reader.Execute()
|
||||
# for k in reader.GetMetaDataKeys(0):
|
||||
# v = reader.GetMetaData(0, k)
|
||||
# print(f'({k}) = = "{v}"')
|
||||
name = dir_path.name
|
||||
|
||||
elif ext in SEQUENCE_EXTS:
|
||||
@@ -140,32 +144,64 @@ def parse_volume_data(filepath: str, series_id=""):
|
||||
itk_volume = sitk.ReadImage(filepath)
|
||||
name = Path(filepath).name.removesuffix(ext)
|
||||
|
||||
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
|
||||
volume = sitk.GetArrayFromImage(itk_volume)
|
||||
# for key in itk_volume.GetMetaDataKeys():
|
||||
# print(f"{key},{itk_volume.GetMetaData(key)}")
|
||||
|
||||
if itk_volume.GetDimension() == 3:
|
||||
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
|
||||
|
||||
meta = {
|
||||
"name": name,
|
||||
"shape": tuple(itk_volume.GetSize()),
|
||||
"spacing": tuple(itk_volume.GetSpacing()),
|
||||
"origin": tuple(itk_volume.GetOrigin()),
|
||||
"direction": tuple(itk_volume.GetDirection()),
|
||||
"is_oriented": True
|
||||
}
|
||||
|
||||
volume = sitk.GetArrayFromImage(itk_volume)
|
||||
|
||||
# transpose ijk to kji
|
||||
if volume.ndim == 4:
|
||||
volume = np.transpose(volume, (2, 1, 0, 3))
|
||||
else:
|
||||
volume = np.transpose(volume)
|
||||
volume = np.expand_dims(volume, axis=-1)
|
||||
|
||||
if volume.ndim == 4:
|
||||
volume = np.transpose(volume, (2, 1, 0, 3))
|
||||
else:
|
||||
volume = np.transpose(volume)
|
||||
# FIXME: not sure...
|
||||
print(itk_volume.GetDirection())
|
||||
direction = np.array(itk_volume.GetDirection())
|
||||
direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \
|
||||
else direction.reshape(4, 4)
|
||||
|
||||
meta = {
|
||||
"name": name,
|
||||
"shape": tuple(itk_volume.GetSize()),
|
||||
"spacing": tuple(itk_volume.GetSpacing()),
|
||||
"origin": tuple(itk_volume.GetOrigin()),
|
||||
"direction": tuple(itk_volume.GetDirection()),
|
||||
}
|
||||
direction = direction[1:, 1:]
|
||||
direction = tuple(direction.flatten())
|
||||
|
||||
for key, value in meta.items():
|
||||
print(f"{key}: {value}")
|
||||
meta = {
|
||||
"name": name,
|
||||
"shape": tuple(itk_volume.GetSize()[:3]),
|
||||
"spacing": tuple(itk_volume.GetSpacing()[:3]),
|
||||
"origin": tuple(itk_volume.GetOrigin()[:3]),
|
||||
"direction": direction,
|
||||
"is_oriented": False
|
||||
}
|
||||
|
||||
volume = sitk.GetArrayFromImage(itk_volume)
|
||||
|
||||
if volume.ndim == 5:
|
||||
volume = np.transpose(volume, (0, 3, 2, 1, 4))
|
||||
else:
|
||||
volume = np.transpose(volume, (0, 3, 2, 1))
|
||||
volume = np.expand_dims(volume, axis=-1)
|
||||
|
||||
return volume, meta
|
||||
|
||||
|
||||
class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.import_volume_data_dialog"
|
||||
bl_label = "Volume Data as Bioxel Layer"
|
||||
bl_description = "Import Volume Data as Bioxel Layer"
|
||||
bl_label = "Volumetric Data as Bioxel Layer"
|
||||
bl_description = "Import Volumetric Data as Bioxel Layer"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
@@ -188,7 +224,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("labels", "Labels", "")]
|
||||
("label", "Labels", "")]
|
||||
) # type: ignore
|
||||
|
||||
bioxel_size: bpy.props.FloatProperty(
|
||||
@@ -215,9 +251,14 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
default=0.01,
|
||||
) # type: ignore
|
||||
|
||||
do_orient: bpy.props.BoolProperty(
|
||||
name="Orient to RAS",
|
||||
default=True,
|
||||
is_time_sequence: bpy.props.BoolProperty(
|
||||
name="Is Time Sequence",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
split_channels: bpy.props.BoolProperty(
|
||||
name="Split Channels",
|
||||
default=False,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
@@ -259,10 +300,8 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
bioxel_size, 4
|
||||
)
|
||||
|
||||
# transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
|
||||
# if self.do_orient else mat_location @ mat_rotation @ mat_scale
|
||||
|
||||
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale
|
||||
transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
|
||||
if meta['is_oriented'] else mat_location @ mat_rotation @ mat_scale
|
||||
|
||||
# Wrapper a Container
|
||||
if not self.container:
|
||||
@@ -308,26 +347,62 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
container_node_group = container.modifiers[0].node_group
|
||||
|
||||
preferences = context.preferences.addons[__package__].preferences
|
||||
loc, rot, sca = transfrom.decompose()
|
||||
|
||||
# TODO: change to transform when 4.2?
|
||||
loc, rot, sca = transfrom.decompose()
|
||||
layer_origin = tuple(loc)
|
||||
layer_rotation = tuple(rot.to_euler())
|
||||
|
||||
def create_layer(volume, layer_name, layer_type="scalar"):
|
||||
grid = vdb.FloatGrid()
|
||||
volume = volume.copy().astype(np.float32)
|
||||
grid.copyFromArray(volume)
|
||||
# grid.transform = vdb.createLinearTransform(transfrom.transposed())
|
||||
grid.name = layer_type
|
||||
def create_layer(volume, layer_name, layer_shape, layer_type="scalar"):
|
||||
if volume.ndim == 4:
|
||||
grids_sequence = []
|
||||
for f in range(volume.shape[0]):
|
||||
print(f"Resampling...")
|
||||
frame = ski.resize(volume[f, :, :, :],
|
||||
layer_shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=volume.dtype.kind != "b")
|
||||
|
||||
vdb_path = save_vdb([grid], context)
|
||||
grid = vdb.FloatGrid()
|
||||
frame = frame.copy().astype(np.float32)
|
||||
grid.copyFromArray(frame)
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
transfrom.transposed())
|
||||
grid.name = layer_type
|
||||
grids_sequence.append([grid])
|
||||
|
||||
# Read VDB
|
||||
print(f"Loading the cache to Blender scene...")
|
||||
bpy.ops.object.volume_import(
|
||||
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
vdb_paths = save_vdbs(grids_sequence, context)
|
||||
|
||||
# Read VDB
|
||||
print(f"Loading the cache to Blender scene...")
|
||||
files = [{"name": str(vdb_path.name), "name": str(vdb_path.name)}
|
||||
for vdb_path in vdb_paths]
|
||||
|
||||
bpy.ops.object.volume_import(filepath=str(vdb_paths[0]), directory=str(vdb_paths[0].parent),
|
||||
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
|
||||
else:
|
||||
volume = ski.resize(volume,
|
||||
layer_shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=volume.dtype.kind != "b")
|
||||
|
||||
grid = vdb.FloatGrid()
|
||||
volume = volume.copy().astype(np.float32)
|
||||
grid.copyFromArray(volume)
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
transfrom.transposed())
|
||||
grid.name = layer_type
|
||||
|
||||
vdb_path = save_vdb([grid], context)
|
||||
|
||||
# Read VDB
|
||||
print(f"Loading the cache to Blender scene...")
|
||||
bpy.ops.object.volume_import(filepath=str(vdb_path),
|
||||
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
|
||||
layer = bpy.context.active_object
|
||||
layer.data.sequence_mode = 'REPEAT'
|
||||
|
||||
# Set props to VDB object
|
||||
layer.name = layer_name
|
||||
@@ -368,7 +443,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
output_node.inputs[0])
|
||||
|
||||
# for compatibility to old vdb
|
||||
to_layer_node.inputs['Not Transfromed'].default_value = True
|
||||
# to_layer_node.inputs['Not Transfromed'].default_value = True
|
||||
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
to_layer_node.inputs['Data Type'].default_value = dtype_index
|
||||
@@ -381,96 +456,118 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
|
||||
return layer
|
||||
|
||||
if self.read_as == "labels":
|
||||
if volume.ndim == 4:
|
||||
volume = np.amax(volume, -1)
|
||||
volume = volume.astype(int)
|
||||
orig_max = int(np.max(volume))
|
||||
orig_min = int(np.min(volume))
|
||||
layer_name = self.layer_name or "Label"
|
||||
|
||||
for i in range(orig_max):
|
||||
label = volume == np.full_like(volume, i+1)
|
||||
print(f"Resampling...")
|
||||
label = ski.resize(label,
|
||||
layer_shape,
|
||||
preserve_range=True,
|
||||
anti_aliasing=False)
|
||||
layer = create_layer(volume=label,
|
||||
layer_name=f"{container_name}_{layer_name}_{i+1}",
|
||||
layer_type="label")
|
||||
|
||||
mask_node = custom_nodes.add_node(container_node_group,
|
||||
'BioxelNodes_MaskByLabel')
|
||||
mask_node.label = f"{layer_name}_{i+1}"
|
||||
mask_node.inputs[0].default_value = layer
|
||||
|
||||
# Connect to output if no output linked
|
||||
output_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
container_node_group.links.new(mask_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||
else:
|
||||
move_node_to_node(mask_node, output_node,
|
||||
(0, -100 * (i+1)))
|
||||
|
||||
else:
|
||||
if volume.ndim == 4:
|
||||
volume = np.amax(volume, -1)
|
||||
# volume = skimage.color.rgb2gray(volume)
|
||||
|
||||
# if volume.dtype.kind == 'u':
|
||||
# imax_in = np.iinfo(volume.dtype).max
|
||||
# volume = np.multiply(volume, 255.0 / imax_in, dtype=np.float32)
|
||||
# elif volume.dtype.kind == 'i':
|
||||
# volume = volume.astype(np.float32)
|
||||
|
||||
# should not change any value!
|
||||
volume = volume.astype(np.float32)
|
||||
|
||||
print(f"Resampling...")
|
||||
volume = ski.resize(volume,
|
||||
layer_shape,
|
||||
anti_aliasing=True)
|
||||
|
||||
orig_max = float(np.max(volume))
|
||||
orig_min = float(np.min(volume))
|
||||
|
||||
scalar_offset = 0
|
||||
if orig_min < 0:
|
||||
scalar_offset = -orig_min
|
||||
volume = volume + np.full_like(volume, scalar_offset)
|
||||
|
||||
layer_name = self.layer_name or "Scalar"
|
||||
layer = create_layer(volume=volume,
|
||||
layer_name=f"{container_name}_{layer_name}",
|
||||
layer_type="scalar")
|
||||
|
||||
layer_node_group = layer.modifiers[0].node_group
|
||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
||||
|
||||
def create_mask_node(layer, node_type, node_label, offset):
|
||||
mask_node = custom_nodes.add_node(container_node_group,
|
||||
'BioxelNodes_MaskByThreshold')
|
||||
mask_node.label = layer_name
|
||||
node_type)
|
||||
mask_node.label = node_label
|
||||
mask_node.inputs[0].default_value = layer
|
||||
|
||||
# Connect to output if no output linked
|
||||
output_node = get_nodes_by_type(container_node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
container_node_group.links.new(mask_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
||||
else:
|
||||
move_node_to_node(mask_node, output_node, (0, -100))
|
||||
move_node_to_node(mask_node, output_node, offset)
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
# change shape as sequence or not
|
||||
if self.is_time_sequence:
|
||||
# 4->5 or 5->5
|
||||
if volume.ndim == 4:
|
||||
# channel as frame
|
||||
volume = volume.transpose(3, 0, 1, 2)
|
||||
volume = np.expand_dims(volume, axis=-1)
|
||||
|
||||
else:
|
||||
# 4->4 or 5->4
|
||||
if volume.ndim == 5:
|
||||
# select frame 0
|
||||
volume = volume[0, :, :, :, :]
|
||||
|
||||
if self.read_as == "label":
|
||||
layer_name = self.layer_name or "Label"
|
||||
volume = np.amax(volume, -1)
|
||||
volume = volume.astype(int)
|
||||
orig_max = int(np.max(volume))
|
||||
orig_min = int(np.min(volume))
|
||||
|
||||
for i in range(orig_max):
|
||||
label = volume == np.full_like(volume, i+1)
|
||||
layer_name_i = f"{layer_name}_{i+1}"
|
||||
|
||||
layer = create_layer(volume=label,
|
||||
layer_name=f"{container_name}_{layer_name_i}",
|
||||
layer_shape=layer_shape,
|
||||
layer_type="label")
|
||||
|
||||
mask_node = create_mask_node(layer=layer,
|
||||
node_type='BioxelNodes_MaskByLabel',
|
||||
node_label=layer_name_i,
|
||||
offset=(0, -100 * (i+1)))
|
||||
|
||||
else:
|
||||
layer_name = self.layer_name or "Scalar"
|
||||
# SHOULD NOT change any value!
|
||||
volume = volume.astype(np.float32)
|
||||
|
||||
if self.split_channels:
|
||||
for i in range(volume.shape[-1]):
|
||||
scalar = volume[:, :, :, :,
|
||||
0] if self.is_time_sequence else volume[:, :, :, i]
|
||||
orig_max = float(np.max(scalar))
|
||||
orig_min = float(np.min(scalar))
|
||||
|
||||
scalar_offset = 0
|
||||
if orig_min < 0:
|
||||
scalar_offset = -orig_min
|
||||
scalar = scalar + np.full_like(scalar, scalar_offset)
|
||||
|
||||
layer_name_i = f"{layer_name}_{i+1}"
|
||||
layer = create_layer(volume=scalar,
|
||||
layer_name=f"{container_name}_{layer_name_i}",
|
||||
layer_shape=layer_shape,
|
||||
layer_type="scalar")
|
||||
|
||||
layer_node_group = layer.modifiers[0].node_group
|
||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
||||
|
||||
mask_node = create_mask_node(layer=layer,
|
||||
node_type='BioxelNodes_MaskByThreshold',
|
||||
node_label=layer_name_i,
|
||||
offset=(0, -100 * (i+1)))
|
||||
|
||||
else:
|
||||
volume = np.amax(volume, -1)
|
||||
orig_max = float(np.max(volume))
|
||||
orig_min = float(np.min(volume))
|
||||
|
||||
scalar_offset = 0
|
||||
if orig_min < 0:
|
||||
scalar_offset = -orig_min
|
||||
volume = volume + np.full_like(volume, scalar_offset)
|
||||
|
||||
layer = create_layer(volume=volume,
|
||||
layer_name=f"{container_name}_{layer_name}",
|
||||
layer_shape=layer_shape,
|
||||
layer_type="scalar")
|
||||
|
||||
layer_node_group = layer.modifiers[0].node_group
|
||||
to_layer_node = layer_node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
to_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
||||
to_layer_node.inputs['Scalar Max'].default_value = orig_max
|
||||
to_layer_node.inputs['Scalar Min'].default_value = orig_min
|
||||
|
||||
mask_node = create_mask_node(layer=layer,
|
||||
node_type='BioxelNodes_MaskByThreshold',
|
||||
node_label=layer_name,
|
||||
offset=(0, -100))
|
||||
|
||||
select_object(container)
|
||||
|
||||
# Change render setting for better result
|
||||
if preferences.do_change_render_setting and is_first_import:
|
||||
@@ -492,7 +589,7 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if self.read_as == "labels":
|
||||
if self.read_as == "label":
|
||||
volume_dtype = "Label"
|
||||
elif self.read_as == "scalar":
|
||||
volume_dtype = "Scalar"
|
||||
@@ -529,6 +626,11 @@ class ImportVolumeDataDialog(bpy.types.Operator):
|
||||
row.prop(self, "orig_spacing")
|
||||
panel.label(text=layer_shape_text)
|
||||
|
||||
panel = layout.box()
|
||||
panel.prop(self, "is_time_sequence")
|
||||
if self.read_as == "scalar":
|
||||
panel.prop(self, "split_channels")
|
||||
|
||||
if self.container == "":
|
||||
panel = layout.box()
|
||||
panel.prop(self, "scene_scale")
|
||||
@@ -550,8 +652,8 @@ def get_series_ids(self, context):
|
||||
|
||||
class ParseVolumeData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.parse_volume_data"
|
||||
bl_label = "Volume Data as Bioxel Layer"
|
||||
bl_description = "Import Volume Data as Bioxel Layer"
|
||||
bl_label = "Volumetric Data as Bioxel Layer"
|
||||
bl_description = "Import Volumetric Data as Bioxel Layer"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
@@ -562,7 +664,7 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("labels", "Labels", "")]
|
||||
("label", "Labels", "")]
|
||||
) # type: ignore
|
||||
|
||||
series_id: bpy.props.EnumProperty(
|
||||
@@ -581,13 +683,17 @@ class ParseVolumeData(bpy.types.Operator):
|
||||
|
||||
print("Collecting Meta Data...")
|
||||
volume, meta = parse_volume_data(self.filepath)
|
||||
if self.read_as == "labels":
|
||||
|
||||
for key, value in meta.items():
|
||||
print(f"{key}: {value}")
|
||||
|
||||
if self.read_as == "label":
|
||||
not_int = volume.dtype.kind != "b" and volume.dtype.kind != "i" and volume.dtype.kind != "u"
|
||||
too_large = np.max(volume) > 100
|
||||
|
||||
if not_int or too_large:
|
||||
self.report(
|
||||
{"WARNING"}, "This volume data does not looks like labels, please check again.")
|
||||
{"WARNING"}, "This volume data does not looks like label, please check again.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# do_orient = ext not in SEQUENCE_EXTS or ext in DICOM_EXTS
|
||||
@@ -704,15 +810,15 @@ class ImportVolumeData():
|
||||
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumeData):
|
||||
bl_idname = "bioxelnodes.import_as_scalar_layer"
|
||||
bl_label = "Import as Scalar"
|
||||
bl_description = "Import Volume Data to Container as Scalar"
|
||||
bl_description = "Import Volumetric Data to Container as Scalar"
|
||||
read_as = "scalar"
|
||||
|
||||
|
||||
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumeData):
|
||||
bl_idname = "bioxelnodes.import_as_label_layer"
|
||||
bl_label = "Import as Labels"
|
||||
bl_description = "Import Volume Data to Container as Label"
|
||||
read_as = "labels"
|
||||
bl_label = "Import as Label"
|
||||
bl_description = "Import Volumetric Data to Container as Label"
|
||||
read_as = "label"
|
||||
|
||||
|
||||
try:
|
||||
@@ -729,40 +835,10 @@ except:
|
||||
...
|
||||
|
||||
|
||||
class AddVolumeData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_volume_data"
|
||||
bl_label = "Import as Bioxel Layer"
|
||||
bl_description = "Import additional Volume Data to Container"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
containers = get_container_from_selection()
|
||||
return len(containers) > 0
|
||||
|
||||
def execute(self, context):
|
||||
containers = get_container_from_selection()
|
||||
|
||||
bpy.ops.bioxelnodes.parse_volume_data(
|
||||
'INVOKE_DEFAULT',
|
||||
filepath=self.filepath,
|
||||
directory=self.directory,
|
||||
container=containers[0].name
|
||||
)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class ExportVolumeData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.export_volume_data"
|
||||
bl_label = "Export Bioxel as VDB"
|
||||
bl_description = "Export Bioxel Layer as VDB"
|
||||
bl_label = "Export Layer as VDB"
|
||||
bl_description = "Export Layer as VDB"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import bpy
|
||||
|
||||
from .operators import (AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange)
|
||||
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer, ImportVolumeData, AddVolumeData
|
||||
from .save import ReLinkNodes, SaveLayers, SaveAllToShare
|
||||
from .utils import get_container_from_selection
|
||||
from .operators import (AddPieCutter, AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange, PickBboxWire, PickMesh, PickVolume)
|
||||
from .io import ExportVolumeData, ImportAsLabelLayer, ImportAsScalarLayer
|
||||
from .save import CleanAllCaches, ReLinkNodes, SaveLayers, SaveStagedData
|
||||
|
||||
|
||||
class ModifyLayer(bpy.types.Menu):
|
||||
class PickFromContainerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_PICK"
|
||||
bl_label = "Pick from Container"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(PickMesh.bl_idname)
|
||||
layout.operator(PickVolume.bl_idname)
|
||||
layout.operator(PickBboxWire.bl_idname)
|
||||
|
||||
|
||||
class ModifyLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_MODIFY_LAYERS"
|
||||
bl_label = "Modify Layer"
|
||||
|
||||
@@ -25,20 +37,21 @@ class AddCutterMenu(bpy.types.Menu):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(AddPlaneCutter.bl_idname)
|
||||
layout.operator(AddCylinderCutter.bl_idname)
|
||||
layout.operator(AddCubeCutter.bl_idname)
|
||||
layout.operator(AddSphereCutter.bl_idname)
|
||||
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")
|
||||
|
||||
|
||||
class ImportLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_LAYERS"
|
||||
bl_label = "Import Volume Data as Bioxel"
|
||||
bl_label = "Import Volumetric Data"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(ImportAsScalarLayer.bl_idname)
|
||||
layout.operator(ImportAsLabelLayer.bl_idname)
|
||||
layout.operator(ImportAsScalarLayer.bl_idname, text="as Scalar")
|
||||
layout.operator(ImportAsLabelLayer.bl_idname, text="as Label")
|
||||
|
||||
|
||||
class BioxelNodesView3DMenu(bpy.types.Menu):
|
||||
@@ -47,9 +60,24 @@ class BioxelNodesView3DMenu(bpy.types.Menu):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(ImportLayerMenu.bl_idname)
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
layout.operator(ConvertToMesh.bl_idname)
|
||||
containers = get_container_from_selection()
|
||||
is_selected = len(containers) > 0
|
||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
||||
layout.operator(ImportAsLabelLayer.bl_idname,
|
||||
text=ImportAsLabelLayer.bl_label+" (Add to)"
|
||||
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
|
||||
layout.separator()
|
||||
layout.operator(AddPlaneCutter.bl_idname)
|
||||
layout.operator(AddCylinderCutter.bl_idname)
|
||||
layout.operator(AddCubeCutter.bl_idname)
|
||||
layout.operator(AddSphereCutter.bl_idname)
|
||||
layout.operator(AddPieCutter.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(PickMesh.bl_idname)
|
||||
layout.operator(PickVolume.bl_idname)
|
||||
layout.operator(PickBboxWire.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(SaveLayers.bl_idname)
|
||||
|
||||
@@ -60,25 +88,51 @@ class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(ImportLayerMenu.bl_idname)
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
layout.operator(ConvertToMesh.bl_idname)
|
||||
containers = get_container_from_selection()
|
||||
is_selected = len(containers) > 0
|
||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
||||
layout.operator(ImportAsLabelLayer.bl_idname,
|
||||
text=ImportAsLabelLayer.bl_label+" (Add to)"
|
||||
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
|
||||
layout.separator()
|
||||
layout.menu(ModifyLayer.bl_idname)
|
||||
layout.operator(AddPlaneCutter.bl_idname)
|
||||
layout.operator(AddCylinderCutter.bl_idname)
|
||||
layout.operator(AddCubeCutter.bl_idname)
|
||||
layout.operator(AddSphereCutter.bl_idname)
|
||||
layout.operator(AddPieCutter.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(PickMesh.bl_idname)
|
||||
layout.operator(PickVolume.bl_idname)
|
||||
layout.operator(PickBboxWire.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(SaveLayers.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(InvertScalar.bl_idname)
|
||||
layout.operator(FillByThreshold.bl_idname)
|
||||
layout.operator(FillByRange.bl_idname)
|
||||
layout.operator(FillByLabel.bl_idname)
|
||||
layout.operator(CombineLabels.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumeData.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR_FILE_IMPORT(self, context):
|
||||
layout = self.layout
|
||||
containers = get_container_from_selection()
|
||||
is_selected = len(containers) > 0
|
||||
|
||||
layout.separator()
|
||||
layout.menu(ImportLayerMenu.bl_idname)
|
||||
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
|
||||
if is_selected else "Volumetric Data as Bioxel (Init)")
|
||||
|
||||
|
||||
def TOPBAR_FILE_EXPORT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.operator(ExportVolumeData.bl_idname)
|
||||
layout.operator(ExportVolumeData.bl_idname,
|
||||
text="Bioxel Layer (.vdb)")
|
||||
|
||||
|
||||
def VIEW3D_OBJECT(self, context):
|
||||
@@ -99,14 +153,19 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.menu(ImportLayerMenu.bl_idname)
|
||||
layout.operator(ExportVolumeData.bl_idname)
|
||||
containers = get_container_from_selection()
|
||||
is_selected = len(containers) > 0
|
||||
|
||||
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
|
||||
if is_selected else ImportLayerMenu.bl_label+" (Init)")
|
||||
layout.separator()
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
layout.operator(ConvertToMesh.bl_idname)
|
||||
layout.menu(PickFromContainerMenu.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(SaveAllToShare.bl_idname)
|
||||
layout.operator(SaveStagedData.bl_idname)
|
||||
layout.operator(ReLinkNodes.bl_idname)
|
||||
layout.separator()
|
||||
layout.operator(CleanAllCaches.bl_idname)
|
||||
|
||||
|
||||
def TOPBAR(self, context):
|
||||
|
||||
@@ -203,6 +203,12 @@ MENU_ITEMS = [
|
||||
'icon': 'MESH_UVSPHERE',
|
||||
'node_type': 'BioxelNodes_SphereObjectCutter',
|
||||
'node_description': '',
|
||||
},
|
||||
{
|
||||
'label': 'Pie Cutter',
|
||||
'icon': 'MESH_CONE',
|
||||
'node_type': 'BioxelNodes_PieObjectCutter',
|
||||
'node_description': '',
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -3,12 +3,12 @@ import random
|
||||
import bpy
|
||||
import pyopenvdb as vdb
|
||||
import numpy as np
|
||||
|
||||
import bmesh
|
||||
from . import skimage as ski
|
||||
from . import scipy
|
||||
from .nodes import custom_nodes
|
||||
from .utils import (get_container, get_container_from_selection, get_container_layers,
|
||||
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb)
|
||||
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, select_object)
|
||||
|
||||
|
||||
def get_layer_name(layer):
|
||||
@@ -105,6 +105,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
|
||||
# add convert to layer node
|
||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
||||
|
||||
not_transformed = base_layer_node.inputs['Not Transfromed'].default_value
|
||||
dtype_index = base_layer_node.inputs['Data Type'].default_value
|
||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
||||
@@ -127,7 +128,7 @@ def deep_copy_layer(vdb_path, base_layer, name):
|
||||
node_group.links.new(copyed_layer_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
# for compatibility to old vdb
|
||||
copyed_layer_node.inputs['Not Transfromed'].default_value = True
|
||||
copyed_layer_node.inputs['Not Transfromed'].default_value = not_transformed
|
||||
copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
||||
200000000)
|
||||
copyed_layer_node.inputs['Data Type'].default_value = dtype_index
|
||||
@@ -291,14 +292,14 @@ class InvertScalar(bpy.types.Operator):
|
||||
"BioxelNodes_MaskByThreshold",
|
||||
inverted_layer_name)
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class FillByThreshold(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
||||
bl_label = "Fill by Threshold"
|
||||
bl_label = "Fill Value by Threshold"
|
||||
bl_description = "Fill Value by Threshold"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@@ -358,7 +359,7 @@ class FillByThreshold(bpy.types.Operator):
|
||||
|
||||
mask_node.inputs[1].default_value = self.threshold
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -372,7 +373,7 @@ class FillByThreshold(bpy.types.Operator):
|
||||
|
||||
class FillByRange(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fill_by_range"
|
||||
bl_label = "Fill by Range"
|
||||
bl_label = "Fill Value by Range"
|
||||
bl_description = "Fill Value by Range"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@@ -441,7 +442,7 @@ class FillByRange(bpy.types.Operator):
|
||||
|
||||
mask_node.inputs[1].default_value = self.from_min
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -455,7 +456,7 @@ class FillByRange(bpy.types.Operator):
|
||||
|
||||
class FillByLabel(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fill_by_label"
|
||||
bl_label = "Fill by Label"
|
||||
bl_label = "Fill Value by Label"
|
||||
bl_description = "Fill Value by Label Area"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@@ -525,7 +526,7 @@ class FillByLabel(bpy.types.Operator):
|
||||
"BioxelNodes_MaskByThreshold",
|
||||
filled_layer_name)
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -585,14 +586,14 @@ class CombineLabels(bpy.types.Operator):
|
||||
"BioxelNodes_MaskByLabel",
|
||||
combined_layer_name)
|
||||
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ConvertToMesh(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.convert_to_mesh"
|
||||
bl_label = "Convert Container To Mesh"
|
||||
bl_description = "Convert Bioxel Container To Mesh"
|
||||
bl_label = "Convert To Mesh"
|
||||
bl_description = "Convert Container To Mesh"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
@@ -633,16 +634,147 @@ class ConvertToMesh(bpy.types.Operator):
|
||||
# constraint=mesh.constraints[0].name, owner='OBJECT')
|
||||
bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name)
|
||||
|
||||
bpy.context.object.active_material_index = 1
|
||||
bpy.ops.object.material_slot_remove()
|
||||
|
||||
bpy.context.view_layer.objects.active = mesh
|
||||
select_object(mesh)
|
||||
|
||||
self.report({"INFO"}, f"Successfully convert to mesh")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PickMesh(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.pick_mesh"
|
||||
bl_label = "Pick Mesh"
|
||||
bl_description = "Pick Container Mesh"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
containers = get_container_from_selection()
|
||||
return len(containers) > 0
|
||||
|
||||
def execute(self, context):
|
||||
containers = get_container_from_selection()
|
||||
|
||||
if len(containers) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container = containers[0]
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
mesh = bpy.context.active_object
|
||||
|
||||
mesh.name = f"Mesh_{container.name}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = mesh.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,
|
||||
"BioxelNodes_PickMesh")
|
||||
|
||||
pick_mesh_node.inputs[0].default_value = container
|
||||
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
select_object(mesh)
|
||||
|
||||
self.report({"INFO"}, f"Successfully picked mesh")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PickVolume(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.pick_volume"
|
||||
bl_label = "Pick Volume"
|
||||
bl_description = "Pick Container Volume"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
containers = get_container_from_selection()
|
||||
return len(containers) > 0
|
||||
|
||||
def execute(self, context):
|
||||
containers = get_container_from_selection()
|
||||
|
||||
if len(containers) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container = containers[0]
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
volume = bpy.context.active_object
|
||||
|
||||
volume.name = f"Volume_{container.name}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = volume.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
pick_volume_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_PickVolume")
|
||||
|
||||
pick_volume_node.inputs[0].default_value = container
|
||||
node_group.links.new(
|
||||
pick_volume_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
select_object(volume)
|
||||
|
||||
self.report({"INFO"}, f"Successfully picked volume")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PickBboxWire(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
||||
bl_label = "Pick Bbox Wire"
|
||||
bl_description = "Pick Container Bbox Wire"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
containers = get_container_from_selection()
|
||||
return len(containers) > 0
|
||||
|
||||
def execute(self, context):
|
||||
containers = get_container_from_selection()
|
||||
|
||||
if len(containers) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container = containers[0]
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bbox_wire = bpy.context.active_object
|
||||
|
||||
bbox_wire.name = f"Wire_{container.name}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = bbox_wire.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
pick_bbox_wire_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_PickBboxWire")
|
||||
|
||||
pick_bbox_wire_node.inputs[0].default_value = container
|
||||
node_group.links.new(
|
||||
pick_bbox_wire_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
select_object(bbox_wire)
|
||||
|
||||
self.report({"INFO"}, f"Successfully picked bbox wire")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddCutter():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@@ -677,6 +809,40 @@ class AddCutter():
|
||||
node_type = "BioxelNodes_SphereObjectCutter"
|
||||
bpy.ops.mesh.primitive_ico_sphere_add(
|
||||
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif self.object_type == "pie":
|
||||
node_type = "BioxelNodes_PieObjectCutter"
|
||||
# Create mesh
|
||||
pie_mesh = bpy.data.meshes.new('Pie')
|
||||
|
||||
# Create object
|
||||
pie = bpy.data.objects.new('Pie', pie_mesh)
|
||||
|
||||
# Link object to scene
|
||||
bpy.context.scene.collection.objects.link(pie)
|
||||
# Get a BMesh representation
|
||||
bm = bmesh.new() # create an empty BMesh
|
||||
bm.from_mesh(pie_mesh) # fill it in from a Mesh
|
||||
|
||||
# Hot to create vertices
|
||||
v_0 = bm.verts.new((0.0, -1.0, 0.0))
|
||||
v_1 = bm.verts.new((-1.0, -1.0, 1.0))
|
||||
v_2 = bm.verts.new((0.0, 1.0, 0.0))
|
||||
v_3 = bm.verts.new((-1.0, 1.0, 1.0))
|
||||
v_4 = bm.verts.new((1.0, -1.0, 1.0))
|
||||
v_5 = bm.verts.new((1.0, 1.0, 1.0))
|
||||
|
||||
# Initialize the index values of this sequence.
|
||||
bm.verts.index_update()
|
||||
|
||||
# How to create a face
|
||||
# it's not necessary to create the edges before, I made it only to show how create
|
||||
# edges too
|
||||
bm.faces.new((v_0, v_1, v_3, v_2))
|
||||
bm.faces.new((v_0, v_2, v_5, v_4))
|
||||
|
||||
# Finish up, write the bmesh back to the mesh
|
||||
bm.to_mesh(pie_mesh)
|
||||
bpy.context.view_layer.objects.active = pie
|
||||
|
||||
cutter = bpy.context.active_object
|
||||
cutter.visible_camera = False
|
||||
@@ -692,7 +858,7 @@ class AddCutter():
|
||||
node_group = modifier.node_group
|
||||
cutter_node = custom_nodes.add_node(node_group, node_type)
|
||||
cutter_node.inputs[0].default_value = cutter
|
||||
|
||||
|
||||
cut_nodes = get_nodes_by_type(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
@@ -715,9 +881,10 @@ class AddCutter():
|
||||
cut_node.inputs[1])
|
||||
|
||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||
select_object(cutter)
|
||||
else:
|
||||
move_node_to_node(cutter_node, output_node, (0, -100))
|
||||
bpy.context.view_layer.objects.active = container
|
||||
select_object(container)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -752,3 +919,11 @@ class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||
bl_description = "Add a Sphere Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "sphere"
|
||||
|
||||
|
||||
class AddPieCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_pie_cutter"
|
||||
bl_label = "Add a Pie Cutter"
|
||||
bl_description = "Add a Pie Cutter to Container"
|
||||
bl_options = {'UNDO'}
|
||||
object_type = "pie"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import re
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from .utils import get_all_layers, get_container, get_container_layers
|
||||
from .utils import copy_to_dir, get_all_layers, get_container, get_container_from_selection, get_container_layers
|
||||
from .nodes import custom_nodes
|
||||
|
||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||
@@ -26,18 +27,18 @@ class ReLinkNodes(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveAllToShare(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_all_to_share"
|
||||
bl_label = "Save All Staged Data"
|
||||
bl_description = "Save all staged data for sharing"
|
||||
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_layer: bpy.props.BoolProperty(
|
||||
name="Save Layer VDB Cache",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
layer_dir: bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
@@ -83,15 +84,18 @@ class SaveAllToShare(bpy.types.Operator):
|
||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
||||
|
||||
if self.save_layer:
|
||||
layers = get_all_layers()
|
||||
for layer in layers:
|
||||
fails = []
|
||||
for layer in get_all_layers():
|
||||
try:
|
||||
save_layer(layer, self.layer_dir)
|
||||
save_layer(layer, self.cache_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
fails.append(layer)
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -108,36 +112,43 @@ class SaveAllToShare(bpy.types.Operator):
|
||||
layout = self.layout
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_layer")
|
||||
panel.prop(self, "layer_dir")
|
||||
panel.prop(self, "cache_dir")
|
||||
panel = layout.box()
|
||||
panel.prop(self, "save_lib")
|
||||
panel.prop(self, "lib_dir")
|
||||
|
||||
|
||||
def save_layer(layer, output_dir):
|
||||
name = layer.name
|
||||
|
||||
pattern = r'\.\d{4}\.'
|
||||
|
||||
# "//"
|
||||
output_dir = bpy.path.abspath(output_dir)
|
||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
||||
|
||||
output_path: Path = Path(output_dir, f"{name}.vdb").resolve()
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = re.search(pattern, source_path.name) is not None
|
||||
name = layer.name if is_sequence else f"{layer.name}.vdb"
|
||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
||||
if is_sequence else Path(output_dir, name).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
shutil.copy(source_path, output_path)
|
||||
copy_to_dir(source_path.parent if is_sequence else source_path,
|
||||
output_path.parent.parent if is_sequence else output_path.parent,
|
||||
new_name=name)
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
layer.data.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
|
||||
class SaveLayers(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_layers"
|
||||
bl_label = "Save Container's Layers"
|
||||
bl_description = "Save Bioxel Layers to Directory."
|
||||
bl_label = "Save Layers"
|
||||
bl_description = "Save Container Layers to Directory."
|
||||
|
||||
layer_dir: bpy.props.StringProperty(
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
@@ -145,24 +156,29 @@ class SaveLayers(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
return True if container else False
|
||||
containers = get_container_from_selection()
|
||||
return len(containers) > 0
|
||||
|
||||
def execute(self, context):
|
||||
container = get_container(bpy.context.active_object)
|
||||
containers = get_container_from_selection()
|
||||
|
||||
if not container:
|
||||
if len(containers) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
for layer in get_container_layers(container):
|
||||
try:
|
||||
save_layer(layer, self.layer_dir)
|
||||
except:
|
||||
self.report(
|
||||
{"WARNING"}, f"Fail to save {layer.name}, skiped")
|
||||
fails = []
|
||||
for container in containers:
|
||||
for layer in get_container_layers(container):
|
||||
try:
|
||||
save_layer(layer, self.cache_dir)
|
||||
except:
|
||||
fails.append(layer)
|
||||
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -170,3 +186,27 @@ class SaveLayers(bpy.types.Operator):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class CleanAllCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_all_caches"
|
||||
bl_label = "Clean All Caches in Temp"
|
||||
bl_description = "Clean all caches saved in temp"
|
||||
|
||||
def execute(self, context):
|
||||
preferences = context.preferences.addons[__package__].preferences
|
||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
self.report({"INFO"}, f"Successfully cleaned caches.")
|
||||
return {'FINISHED'}
|
||||
except:
|
||||
self.report({"WARNING"},
|
||||
"Fail to clean caches, 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?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -3,6 +3,45 @@ import mathutils
|
||||
from pathlib import Path
|
||||
import pyopenvdb as vdb
|
||||
from uuid import uuid4
|
||||
import shutil
|
||||
|
||||
|
||||
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 copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
source = Path(source_path)
|
||||
target = Path(dir_path)
|
||||
|
||||
# Check if the source exists
|
||||
if not source.exists():
|
||||
raise FileNotFoundError
|
||||
|
||||
# Check if the target exists
|
||||
if not target.exists():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
target_path = target / new_name if new_name else target / source.name
|
||||
# If source is a file, copy it to the target directory
|
||||
if source.is_file():
|
||||
try:
|
||||
shutil.copy(source, target_path)
|
||||
except shutil.SameFileError:
|
||||
if exist_ok:
|
||||
pass
|
||||
else:
|
||||
raise shutil.SameFileError
|
||||
# If source is a directory, copy its contents to the target directory
|
||||
elif source.is_dir():
|
||||
shutil.copytree(source, target_path, dirs_exist_ok=exist_ok)
|
||||
|
||||
if not target_path.exists():
|
||||
raise Exception
|
||||
|
||||
|
||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||
@@ -124,13 +163,35 @@ def save_vdb(grids, context):
|
||||
return vdb_path
|
||||
|
||||
|
||||
def save_vdbs(grids_sequence, context):
|
||||
preferences = context.preferences.addons[__package__].preferences
|
||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
vdb_name = str(uuid4())
|
||||
vdb_dir_path = Path(cache_dir, vdb_name)
|
||||
vdb_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
vdb_paths = []
|
||||
for f, grids in enumerate(grids_sequence):
|
||||
vdb_path = Path(vdb_dir_path, f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
|
||||
print(f"Storing the VDB file ({str(vdb_path)})...")
|
||||
vdb.write(str(vdb_path), grids=grids)
|
||||
vdb_paths.append(vdb_path)
|
||||
|
||||
return vdb_paths
|
||||
|
||||
|
||||
def get_container_from_selection():
|
||||
containers = []
|
||||
for obj in bpy.context.selected_objects:
|
||||
if get_container(obj):
|
||||
containers.append(obj)
|
||||
# if bpy.context.active_object:
|
||||
# if bpy.context.active_object.get('bioxel_layer'):
|
||||
# if bpy.context.active_object.parent.get('bioxel_container'):
|
||||
# containers.append(bpy.context.active_object.parent)
|
||||
|
||||
return containers
|
||||
return list(set(containers))
|
||||
|
||||
|
||||
def get_container(current_obj):
|
||||
|
||||
Reference in New Issue
Block a user