feat: layer list system
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import bpy
|
||||
|
||||
|
||||
import bmesh
|
||||
|
||||
from ..nodes import custom_nodes
|
||||
from ..customnodes.nodes import AddCustomNode
|
||||
from ..bioxel.io import load_container, save_container
|
||||
from ..bioxelutils.layer import get_all_layer_objs
|
||||
from ..bioxelutils.container import (container_to_obj, obj_to_container,
|
||||
get_container_objs_from_selection)
|
||||
from ..bioxelutils.node import get_nodes_by_type, move_node_between_nodes, move_node_to_node
|
||||
from .utils import change_render_setting, get_cache_dir, get_preferences, select_object
|
||||
from ..bioxelutils.container import container_to_obj, obj_to_container
|
||||
from ..bioxelutils.utils import (get_container_layer_objs, get_container_obj, get_nodes_by_type,
|
||||
move_node_between_nodes,
|
||||
move_node_to_node,
|
||||
get_all_layer_objs)
|
||||
|
||||
from .utils import get_cache_dir, select_object
|
||||
|
||||
|
||||
class SaveContainer(bpy.types.Operator):
|
||||
@@ -23,19 +25,14 @@ class SaveContainer(bpy.types.Operator):
|
||||
|
||||
filename_ext = ".bioxel"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
return len(container_objs) > 0
|
||||
|
||||
def execute(self, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if len(container_objs) == 0:
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container = obj_to_container(container_objs[0])
|
||||
container = obj_to_container(container_obj)
|
||||
save_path = f"{self.filepath.split('.')[0]}.bioxel"
|
||||
save_container(container, save_path, overwrite=True)
|
||||
|
||||
@@ -82,20 +79,13 @@ class LoadContainer(bpy.types.Operator):
|
||||
class PickObject():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
return len(container_objs) > 0
|
||||
|
||||
def execute(self, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if len(container_objs) == 0:
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container_obj = container_objs[0]
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
obj = bpy.context.active_object
|
||||
@@ -124,6 +114,7 @@ class PickMesh(bpy.types.Operator, PickObject):
|
||||
bl_idname = "bioxelnodes.pick_mesh"
|
||||
bl_label = "Pick Mesh"
|
||||
bl_description = "Pick Container Mesh"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
object_type = "Mesh"
|
||||
|
||||
|
||||
@@ -131,6 +122,7 @@ class PickVolume(bpy.types.Operator, PickObject):
|
||||
bl_idname = "bioxelnodes.pick_volume"
|
||||
bl_label = "Pick Volume"
|
||||
bl_description = "Pick Container Volume"
|
||||
bl_icon = "OUTLINER_OB_VOLUME"
|
||||
object_type = "Volume"
|
||||
|
||||
|
||||
@@ -138,45 +130,34 @@ class PickBboxWire(bpy.types.Operator, PickObject):
|
||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
||||
bl_label = "Pick Bbox Wire"
|
||||
bl_description = "Pick Container Bbox Wire"
|
||||
bl_icon = "MESH_CUBE"
|
||||
object_type = "BboxWire"
|
||||
|
||||
|
||||
class AddCutter():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
return len(container_objs) > 0
|
||||
|
||||
def execute(self, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if len(container_objs) == 0:
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container_obj = container_objs[0]
|
||||
|
||||
if self.object_type == "plane":
|
||||
node_type = "BioxelNodes_PlaneObjectCutter"
|
||||
# TODO: do not use operator to create obj
|
||||
if self.cutter_type == "plane":
|
||||
bpy.ops.mesh.primitive_plane_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif self.object_type == "cylinder":
|
||||
node_type = "BioxelNodes_CylinderObjectCutter"
|
||||
elif self.cutter_type == "cylinder":
|
||||
bpy.ops.mesh.primitive_cylinder_add(
|
||||
radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
bpy.context.object.rotation_euler[0] = container_obj.rotation_euler[0]
|
||||
elif self.object_type == "cube":
|
||||
node_type = "BioxelNodes_CubeObjectCutter"
|
||||
elif self.cutter_type == "cube":
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif self.object_type == "sphere":
|
||||
node_type = "BioxelNodes_SphereObjectCutter"
|
||||
elif self.cutter_type == "sphere":
|
||||
bpy.ops.mesh.primitive_ico_sphere_add(
|
||||
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
elif self.object_type == "pie":
|
||||
node_type = "BioxelNodes_PieObjectCutter"
|
||||
elif self.cutter_type == "pie":
|
||||
# Create mesh
|
||||
pie_mesh = bpy.data.meshes.new('Pie')
|
||||
|
||||
@@ -211,6 +192,7 @@ class AddCutter():
|
||||
bpy.context.view_layer.objects.active = pie
|
||||
|
||||
cutter_obj = bpy.context.active_object
|
||||
cutter_obj.location = container_obj.location
|
||||
cutter_obj.visible_camera = False
|
||||
cutter_obj.visible_diffuse = False
|
||||
cutter_obj.visible_glossy = False
|
||||
@@ -220,16 +202,23 @@ class AddCutter():
|
||||
cutter_obj.hide_render = True
|
||||
cutter_obj.display_type = 'WIRE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
cutter_node = custom_nodes.add_node(node_group, node_type)
|
||||
cutter_node.inputs[0].default_value = cutter_obj
|
||||
|
||||
cut_nodes = get_nodes_by_type(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
if len(cut_nodes) == 0:
|
||||
cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut')
|
||||
cutter_node = custom_nodes.add_node(node_group,
|
||||
"BioxelNodes_ObjectCutter")
|
||||
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')
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(cut_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
@@ -247,44 +236,132 @@ class AddCutter():
|
||||
cut_node.inputs[1])
|
||||
|
||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||
select_object(cutter_obj)
|
||||
else:
|
||||
move_node_to_node(cutter_node, output_node, (0, -100))
|
||||
select_object(container_obj)
|
||||
bpy.ops.bioxelnodes.object_cutter('INVOKE_DEFAULT',
|
||||
cutter_obj_name=cutter_obj.name,
|
||||
cutter_type=self.cutter_type.capitalize())
|
||||
|
||||
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"
|
||||
object_type = "plane"
|
||||
bl_icon = "MESH_PLANE"
|
||||
cutter_type = "plane"
|
||||
|
||||
|
||||
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
||||
bl_label = "Add a Cylinder Cutter"
|
||||
bl_description = "Add a Cylinder Cutter to Container"
|
||||
object_type = "cylinder"
|
||||
bl_icon = "MESH_CYLINDER"
|
||||
cutter_type = "cylinder"
|
||||
|
||||
|
||||
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_cube_cutter"
|
||||
bl_label = "Add a Cube Cutter"
|
||||
bl_description = "Add a Cube Cutter to Container"
|
||||
object_type = "cube"
|
||||
bl_icon = "MESH_CUBE"
|
||||
cutter_type = "cube"
|
||||
|
||||
|
||||
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_sphere_cutter"
|
||||
bl_label = "Add a Sphere Cutter"
|
||||
bl_description = "Add a Sphere Cutter to Container"
|
||||
object_type = "sphere"
|
||||
bl_icon = "MESH_UVSPHERE"
|
||||
cutter_type = "sphere"
|
||||
|
||||
|
||||
class AddPieCutter(bpy.types.Operator, AddCutter):
|
||||
bl_idname = "bioxelnodes.add_pie_cutter"
|
||||
bl_label = "Add a Pie Cutter"
|
||||
bl_description = "Add a Pie Cutter to Container"
|
||||
object_type = "pie"
|
||||
bl_icon = "MESH_CONE"
|
||||
cutter_type = "pie"
|
||||
|
||||
|
||||
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."
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
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)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -5,19 +5,19 @@ import threading
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from ..exceptions import CancelledByUser
|
||||
from ..props import BIOXELNODES_Series
|
||||
from ..bioxel.layer import Layer
|
||||
from ..bioxelutils.layer import (get_all_layer_objs,
|
||||
get_layer_obj)
|
||||
from ..bioxelutils.utils import (get_all_layer_objs, get_container_obj,
|
||||
get_layer_obj,
|
||||
get_container_objs_from_selection)
|
||||
from ..bioxelutils.container import (Container,
|
||||
add_layers,
|
||||
container_to_obj,
|
||||
get_container_objs_from_selection)
|
||||
container_to_obj)
|
||||
from ..bioxel.layer import Layer
|
||||
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
|
||||
get_ext, parse_volumetric_data)
|
||||
from .utils import (change_render_setting, get_cache_dir, get_preferences,
|
||||
|
||||
from .utils import (get_cache_dir,
|
||||
progress_update, progress_bar, select_object)
|
||||
|
||||
# 3rd-party
|
||||
@@ -78,20 +78,30 @@ class ImportVolumetricData():
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_idname = "bioxelnodes.import_as_scalar_layer"
|
||||
class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_idname = "bioxelnodes.import_as_scalar"
|
||||
bl_label = "Import as Scalar"
|
||||
bl_description = "Import Volumetric Data to Container as Scalar"
|
||||
bl_icon = "EVENT_S"
|
||||
read_as = "scalar"
|
||||
|
||||
|
||||
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_idname = "bioxelnodes.import_as_label_layer"
|
||||
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_idname = "bioxelnodes.import_as_label"
|
||||
bl_label = "Import as Label"
|
||||
bl_description = "Import Volumetric Data to Container as Label"
|
||||
bl_icon = "EVENT_L"
|
||||
read_as = "label"
|
||||
|
||||
|
||||
class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
|
||||
bl_idname = "bioxelnodes.import_as_color"
|
||||
bl_label = "Import as Label"
|
||||
bl_description = "Import Volumetric Data to Container as Label"
|
||||
bl_icon = "EVENT_C"
|
||||
read_as = "color"
|
||||
|
||||
|
||||
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
||||
bl_idname = "BIOXELNODES_FH_ImportVolumetricData"
|
||||
bl_label = "File handler for dicom import"
|
||||
@@ -100,7 +110,16 @@ class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
||||
|
||||
@classmethod
|
||||
def poll_drop(cls, context):
|
||||
return (context.area and context.area.type == 'VIEW_3D')
|
||||
if not context.area:
|
||||
return False
|
||||
|
||||
if context.area.type == 'VIEW_3D':
|
||||
return True
|
||||
elif context.area.type == 'NODE_EDITOR':
|
||||
container_obj = get_container_obj(context.object)
|
||||
return container_obj is not None
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_series_ids(self, context):
|
||||
@@ -125,7 +144,6 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
meta = None
|
||||
thread = None
|
||||
_timer = None
|
||||
container_obj_name = ""
|
||||
|
||||
progress: bpy.props.FloatProperty(name="Progress",
|
||||
options={"SKIP_SAVE"},
|
||||
@@ -144,7 +162,8 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", "")]) # type: ignore
|
||||
("label", "Labels", ""),
|
||||
("color", "Color", "")]) # type: ignore
|
||||
|
||||
series_id: bpy.props.EnumProperty(name="Select Series",
|
||||
items=get_series_ids) # type: ignore
|
||||
@@ -153,6 +172,10 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
type=BIOXELNODES_Series) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if not self.filepath:
|
||||
self.report({"WARNING"}, "No file selected.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = get_ext(data_path)
|
||||
if ext not in SUPPORT_EXTS:
|
||||
@@ -207,7 +230,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
# Check if user press 'ESC'
|
||||
if event.type == 'ESC':
|
||||
self.is_cancelled = True
|
||||
progress_update(context, 0, "Canceling...")
|
||||
progress_update(context, 0.0, "Canceling...")
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
# Check if is the timer time
|
||||
@@ -227,6 +250,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
# Remove the progress bar from status bar
|
||||
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||
progress_update(context, 1.0)
|
||||
|
||||
# Check if thread is cancelled by user
|
||||
if self.is_cancelled:
|
||||
@@ -248,23 +272,32 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
|
||||
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),
|
||||
1)
|
||||
|
||||
min_size = min(orig_spacing[0],
|
||||
orig_spacing[1], orig_spacing[2])
|
||||
bioxel_size = max(min_size, 1.0)
|
||||
|
||||
layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
|
||||
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)
|
||||
# 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
|
||||
|
||||
if self.container_obj_name:
|
||||
container_obj = bpy.data.objects.get(self.container_obj_name)
|
||||
if context.area.type == "NODE_EDITOR":
|
||||
container_obj = context.object
|
||||
container_name = container_obj.name
|
||||
container_obj_name = container_name
|
||||
else:
|
||||
container_name = self.meta['name']
|
||||
container_obj_name = ""
|
||||
|
||||
series_id = self.series_id if self.series_id != "empty" else ""
|
||||
bpy.ops.bioxelnodes.import_volumetric_data_dialog(
|
||||
@@ -278,7 +311,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
series_id=series_id,
|
||||
frame_count=self.meta['frame_count'],
|
||||
channel_count=self.meta['channel_count'],
|
||||
container_obj_name=self.container_obj_name,
|
||||
container_obj_name=container_obj_name,
|
||||
read_as=self.read_as,
|
||||
scene_scale=scene_scale
|
||||
)
|
||||
@@ -287,18 +320,20 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
if not self.filepath:
|
||||
if context.window_manager.bioxelnodes_progress_factor < 1:
|
||||
print("A process is executing, please wait for it to finish.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not self.filepath:
|
||||
return self.execute(context)
|
||||
|
||||
data_path = Path(self.filepath).resolve()
|
||||
ext = get_ext(data_path)
|
||||
|
||||
container_objs = get_container_objs_from_selection()
|
||||
if len(container_objs) > 0:
|
||||
self.container_obj_name = container_objs[0].name
|
||||
|
||||
title = f"Add to **{self.container_obj_name}**" \
|
||||
if self.container_obj_name != "" else f"Init a Container"
|
||||
if context.area.type == "NODE_EDITOR":
|
||||
title = f"Add to **{context.object.name}**"
|
||||
else:
|
||||
title = "Init a Container"
|
||||
|
||||
# Series Selection
|
||||
if ext in DICOM_EXTS:
|
||||
@@ -367,8 +402,7 @@ class ParseVolumetricData(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
if self.skip_read_as:
|
||||
self.execute(context)
|
||||
return {'RUNNING_MODAL'}
|
||||
return self.execute(context)
|
||||
else:
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
@@ -429,7 +463,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
read_as: bpy.props.EnumProperty(name="Read as",
|
||||
default="scalar",
|
||||
items=[("scalar", "Scalar", ""),
|
||||
("label", "Labels", "")]) # type: ignore
|
||||
("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,
|
||||
@@ -540,12 +575,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
try:
|
||||
layer = Layer(data=data == np.full_like(data, i+1),
|
||||
name=name_i,
|
||||
kind=kind,
|
||||
affine=affine)
|
||||
kind=kind)
|
||||
|
||||
layer.resize(shape=shape,
|
||||
progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except CancelledByUser:
|
||||
return
|
||||
@@ -553,6 +589,64 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
self.has_error = e
|
||||
return
|
||||
|
||||
if kind == "color":
|
||||
|
||||
if np.issubdtype(np.uint8, data.dtype):
|
||||
data = np.multiply(data, 1.0 / 256,
|
||||
dtype=np.float32)
|
||||
elif data.dtype.kind in ['u', 'i']:
|
||||
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)
|
||||
|
||||
# Convert the normalized array to float dtype
|
||||
data = data.astype(np.float32)
|
||||
else:
|
||||
data = data.astype(np.float32)
|
||||
|
||||
name = self.layer_name or "Color"
|
||||
if data.shape[4] == 1:
|
||||
data = np.repeat(data, repeats=3, axis=4)
|
||||
elif data.shape[4] == 2:
|
||||
d_shape = list(data.shape)
|
||||
d_shape = d_shape[:4] + [1]
|
||||
zore = np.zeros(tuple(d_shape), dtype=np.float32)
|
||||
data = np.concatenate((data, zore), axis=-1)
|
||||
elif data.shape[4] > 3:
|
||||
data = data[:, :, :, :, :3]
|
||||
|
||||
if cancel():
|
||||
return
|
||||
|
||||
progress_update(context, 0.2,
|
||||
f"Processing {name}...")
|
||||
progress_callback = progress_callback_factory(name,
|
||||
0.2,
|
||||
0.7)
|
||||
|
||||
try:
|
||||
layer = Layer(data=data,
|
||||
name=name,
|
||||
kind=kind)
|
||||
|
||||
layer.resize(shape=shape,
|
||||
progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except CancelledByUser:
|
||||
return
|
||||
except Exception as e:
|
||||
self.has_error = e
|
||||
return
|
||||
|
||||
elif kind == "scalar":
|
||||
name = self.layer_name or "Scalar"
|
||||
|
||||
@@ -573,12 +667,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
try:
|
||||
layer = Layer(data=data[:, :, :, :, i:i+1],
|
||||
name=name_i,
|
||||
kind=kind,
|
||||
affine=affine)
|
||||
kind=kind)
|
||||
|
||||
layer.resize(shape=shape,
|
||||
progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except CancelledByUser:
|
||||
return
|
||||
@@ -598,12 +693,13 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
try:
|
||||
layer = Layer(data=data,
|
||||
name=name,
|
||||
kind=kind,
|
||||
affine=affine)
|
||||
kind=kind)
|
||||
|
||||
layer.resize(shape=shape,
|
||||
progress_callback=progress_callback)
|
||||
|
||||
layer.affine = affine
|
||||
|
||||
layers.append(layer)
|
||||
except CancelledByUser:
|
||||
return
|
||||
@@ -634,7 +730,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
def modal(self, context, event):
|
||||
if event.type == 'ESC':
|
||||
self.is_cancelled = True
|
||||
progress_update(context, 0, "Canceling...")
|
||||
progress_update(context, 0.0, "Canceling...")
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
if event.type != 'TIMER':
|
||||
@@ -647,6 +743,7 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
self.thread.join()
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||
progress_update(context, 1.0)
|
||||
|
||||
if self.is_cancelled:
|
||||
self.report({"WARNING"}, "Canncelled by user.")
|
||||
@@ -672,7 +769,8 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
container_obj=container_obj,
|
||||
cache_dir=get_cache_dir(context))
|
||||
else:
|
||||
container = Container(name=self.container_name,
|
||||
name = self.container_name or "Container"
|
||||
container = Container(name=name,
|
||||
layers=self.layers)
|
||||
|
||||
container_obj = container_to_obj(container,
|
||||
@@ -683,7 +781,10 @@ class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||
|
||||
# Change render setting for better result
|
||||
if is_first_import:
|
||||
change_render_setting(context)
|
||||
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||
preset="slice_viewer")
|
||||
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||
preset="cycles_preview")
|
||||
|
||||
self.report({"INFO"}, "Successfully Imported")
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
from ..bioxel.layer import Layer
|
||||
from ..bioxelutils.node import get_nodes_by_type
|
||||
from ..bioxelutils.container import add_layers, get_container_obj
|
||||
from ..bioxelutils.layer import obj_to_layer, get_container_layer_objs, get_layer_obj
|
||||
from .utils import get_preferences, select_object
|
||||
|
||||
|
||||
def get_layer_prop_value(layer_obj: bpy.types.Object, prop: str):
|
||||
try:
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
return layer_node.inputs[prop].default_value
|
||||
except:
|
||||
return None
|
||||
from ..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.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
|
||||
|
||||
|
||||
def get_label_layer_selection(self, context):
|
||||
@@ -33,78 +29,203 @@ def get_label_layer_selection(self, context):
|
||||
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():
|
||||
bl_options = {'UNDO'}
|
||||
base_obj: bpy.types.Object = None
|
||||
layer_obj_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN"}) # type: ignore
|
||||
|
||||
def add_layer(self, context, layer):
|
||||
preferences = get_preferences(context)
|
||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
||||
container_obj = self.base_obj.parent
|
||||
@property
|
||||
def layer_obj(self):
|
||||
return bpy.data.objects.get(self.layer_obj_name)
|
||||
|
||||
add_layers([layer],
|
||||
container_obj,
|
||||
cache_dir)
|
||||
@property
|
||||
def is_lost(self):
|
||||
if self.layer_obj is None:
|
||||
return None
|
||||
|
||||
select_object(container_obj)
|
||||
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:
|
||||
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
|
||||
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)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.base_obj = get_layer_obj(bpy.context.active_object)
|
||||
self.nodes_file = custom_nodes.nodes_file
|
||||
self.node_type = "BioxelNodes_FetchLayer"
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class ScalarOperator(LayerOperator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
label_objs = [obj for obj in context.selected_ids
|
||||
if get_layer_prop_value(obj, "kind") == "scalar"]
|
||||
return True if len(label_objs) > 0 else False
|
||||
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
|
||||
|
||||
|
||||
class LabelOperator(LayerOperator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
label_objs = [obj for obj in context.selected_ids
|
||||
if get_layer_prop_value(obj, "kind") == "label"]
|
||||
return True if len(label_objs) > 0 else False
|
||||
class ModifyLayerOperator(LayerOperator):
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
"""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))
|
||||
|
||||
class SignScalar(bpy.types.Operator, ScalarOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Scalar"
|
||||
bl_description = "Sign the scalar value"
|
||||
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
|
||||
layer_obj_name=layer_obj.name)
|
||||
|
||||
def execute(self, context):
|
||||
base_layer = obj_to_layer(self.base_obj)
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
modified_layer = base_layer.copy()
|
||||
modified_layer.data = -base_layer.data
|
||||
modified_layer.name = f"{base_layer.name}_Signed"
|
||||
if self.is_lost:
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
orig_layer = obj_to_layer(self.layer_obj)
|
||||
new_layer = self.layer_operate(orig_layer)
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class FillOperator(ScalarOperator):
|
||||
def invoke(self, context, event):
|
||||
self.base_obj = get_layer_obj(bpy.context.active_object)
|
||||
scalar_min = get_layer_prop_value(self.base_obj, "min")
|
||||
self.fill_value = min(scalar_min, 0)
|
||||
context.window_manager.invoke_props_dialog(self, width=400)
|
||||
return {'RUNNING_MODAL'}
|
||||
class ResampleScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
bl_idname = "bioxelnodes.resample_scalar"
|
||||
bl_label = "Resample Scalar"
|
||||
bl_description = "Resample Scalar"
|
||||
bl_icon = "ALIASED"
|
||||
|
||||
|
||||
class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
||||
bl_label = "Fill Value by Threshold"
|
||||
bl_description = "Fill Value by Threshold"
|
||||
|
||||
threshold: bpy.props.FloatProperty(
|
||||
name="Threshold",
|
||||
soft_min=0, soft_max=1024,
|
||||
default=128,
|
||||
bioxel_size: bpy.props.FloatProperty(
|
||||
name="Bioxel Size",
|
||||
soft_min=0.1, soft_max=10.0,
|
||||
default=1,
|
||||
) # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def get_new_shape(orig_shape, orig_size, new_size):
|
||||
return (int(orig_shape[0]*orig_size/new_size),
|
||||
int(orig_shape[1]*orig_size/new_size),
|
||||
int(orig_shape[2]*orig_size/new_size))
|
||||
|
||||
def 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],
|
||||
self.bioxel_size)
|
||||
|
||||
modified_layer.resize(new_shape)
|
||||
modified_layer.name = f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
|
||||
return modified_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")
|
||||
new_shape = self.get_new_shape(orig_shape,
|
||||
orig_size,
|
||||
self.bioxel_size)
|
||||
|
||||
orig_shape = tuple(orig_shape)
|
||||
bioxel_count = new_shape[0] * new_shape[1] * new_shape[2]
|
||||
|
||||
layer_shape_text = f"Shape from {str(orig_shape)} to {str(new_shape)}"
|
||||
if bioxel_count > 100000000:
|
||||
layer_shape_text += "**TOO LARGE!**"
|
||||
|
||||
layout = self.layout
|
||||
layout.prop(self, "bioxel_size")
|
||||
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
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title=f"Resample {self.layer_obj.name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class SignScalar(bpy.types.Operator, ModifyLayerOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Scalar"
|
||||
bl_description = "Sign the scalar 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 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}?")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FillOperator(ModifyLayerOperator):
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
soft_min=0, soft_max=1024.0,
|
||||
@@ -112,30 +233,51 @@ class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||
) # type: ignore
|
||||
|
||||
invert: bpy.props.BoolProperty(
|
||||
name="Invert Area",
|
||||
name="Invert Aera",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
base_layer = obj_to_layer(self.base_obj)
|
||||
def invoke(self, context, event):
|
||||
if self.layer_obj:
|
||||
scalar_min = get_layer_prop_value(self.layer_obj, "min")
|
||||
|
||||
data = np.amax(base_layer.data, -1)
|
||||
self.fill_value = min(scalar_min, 0)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=400,
|
||||
title=f"Fill {self.layer_obj.name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FillByThreshold(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
||||
bl_label = "Fill Value by Threshold"
|
||||
bl_description = "Fill Value by Threshold"
|
||||
bl_icon = "EMPTY_SINGLE_ARROW"
|
||||
|
||||
threshold: bpy.props.FloatProperty(
|
||||
name="Threshold",
|
||||
soft_min=0, soft_max=1024,
|
||||
default=128,
|
||||
) # type: ignore
|
||||
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = data <= self.threshold \
|
||||
if self.invert else data > self.threshold
|
||||
|
||||
modified_layer = base_layer.copy()
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{base_layer.name}_{self.threshold}-Filled"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
|
||||
return {'FINISHED'}
|
||||
modified_layer.name = f"{orig_layer.name}_F-{self.threshold}"
|
||||
return modified_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_icon = "IPO_CONSTANT"
|
||||
|
||||
from_min: bpy.props.FloatProperty(
|
||||
name="From Min",
|
||||
@@ -149,82 +291,52 @@ class FillByRange(bpy.types.Operator, FillOperator):
|
||||
default=256,
|
||||
) # type: ignore
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
soft_min=0, soft_max=1024.0,
|
||||
default=0,
|
||||
) # type: ignore
|
||||
|
||||
invert: bpy.props.BoolProperty(
|
||||
name="Invert Area",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
base_layer = obj_to_layer(self.base_obj)
|
||||
|
||||
data = np.amax(base_layer.data, -1)
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
|
||||
(data > self.from_min) & (data < self.from_max)
|
||||
|
||||
modified_layer = base_layer.copy()
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{base_layer.name}_{self.from_min}-{self.from_max}-Filled"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
return {'FINISHED'}
|
||||
modified_layer.name = f"{orig_layer.name}_F-{self.from_min}-{self.from_max}"
|
||||
return modified_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_icon = "MESH_CAPSULE"
|
||||
|
||||
label_obj_name: bpy.props.EnumProperty(
|
||||
name="Label Layer",
|
||||
items=get_label_layer_selection
|
||||
) # type: ignore
|
||||
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
|
||||
items=get_label_layer_selection) # type: ignore
|
||||
|
||||
fill_value: bpy.props.FloatProperty(
|
||||
name="Fill Value",
|
||||
soft_min=0, soft_max=1024.0,
|
||||
default=0,
|
||||
) # type: ignore
|
||||
|
||||
invert: bpy.props.BoolProperty(
|
||||
name="Invert Label",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
def layer_operate(self, orig_layer: Layer):
|
||||
label_obj = bpy.data.objects.get(self.label_obj_name)
|
||||
if not label_obj:
|
||||
self.report({"WARNING"}, "Cannot find any label layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
base_layer = obj_to_layer(self.base_obj)
|
||||
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(base_layer.shape)
|
||||
label_layer.resize(orig_layer.shape)
|
||||
mask = np.amax(label_layer.data, -1)
|
||||
if self.invert:
|
||||
mask = 1 - mask
|
||||
|
||||
modified_layer = base_layer.copy()
|
||||
modified_layer = orig_layer.copy()
|
||||
modified_layer.fill(self.fill_value, mask)
|
||||
modified_layer.name = f"{base_layer.name}_{label_layer.name}-Filled"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
return {'FINISHED'}
|
||||
modified_layer.name = f"{orig_layer.name}_F-{label_layer.name}"
|
||||
return modified_layer
|
||||
|
||||
|
||||
class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||
class CombineLabels(bpy.types.Operator, ModifyLayerOperator):
|
||||
bl_idname = "bioxelnodes.combine_labels"
|
||||
bl_label = "Combine Labels"
|
||||
bl_description = "Combine all selected labels"
|
||||
bl_icon = "MOD_BUILD"
|
||||
|
||||
def execute(self, context):
|
||||
label_objs = [obj for obj in context.selected_ids
|
||||
label_objs = [obj for obj in get_selected_objs_in_node_tree(context)
|
||||
if get_layer_prop_value(obj, "kind") == "label"]
|
||||
|
||||
if len(label_objs) < 2:
|
||||
@@ -240,11 +352,173 @@ class CombineLabels(bpy.types.Operator, LabelOperator):
|
||||
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)
|
||||
modified_layer.data = np.maximum(
|
||||
modified_layer.data, label_layer.data)
|
||||
label_names.append(label_layer.name)
|
||||
|
||||
modified_layer.name = f"{'-'.join(label_names)}-Combined"
|
||||
modified_layer.name = f"C-{'-'.join(label_names)}"
|
||||
|
||||
self.add_layer(context, modified_layer)
|
||||
self.add_layer_node(context, modified_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"
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
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'}
|
||||
|
||||
# "//"
|
||||
output_dir = bpy.path.abspath(self.cache_dir)
|
||||
source_dir = bpy.path.abspath(self.layer_obj.data.filepath)
|
||||
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = self.layer_obj.data.is_sequence
|
||||
|
||||
name = self.layer_obj.name if is_sequence else f"{self.layer_obj.name}.vdb"
|
||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
||||
if is_sequence else Path(output_dir, name).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
copy_to_dir(source_path.parent if is_sequence else source_path,
|
||||
output_path.parent.parent if is_sequence else output_path.parent,
|
||||
new_name=name)
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
self.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)
|
||||
|
||||
|
||||
class RenameLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.rename_layer"
|
||||
bl_label = "Rename Layer"
|
||||
bl_description = "Rename Layer"
|
||||
bl_icon = "FILE_FONT"
|
||||
|
||||
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)
|
||||
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
self.layer_obj.data.filepath)).resolve()
|
||||
|
||||
if cache_filepath.is_file():
|
||||
if self.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)
|
||||
|
||||
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?")
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import re
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
|
||||
from ..utils import copy_to_dir
|
||||
from .utils import get_cache_dir
|
||||
from ..nodes import custom_nodes
|
||||
from ..bioxelutils.container import get_container_objs_from_selection
|
||||
from ..bioxelutils.layer import get_all_layer_objs, get_container_layer_objs
|
||||
from ..bioxelutils.utils import (get_container_objs_from_selection,
|
||||
get_all_layer_objs,
|
||||
get_container_layer_objs)
|
||||
|
||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
||||
|
||||
@@ -32,30 +29,6 @@ class ReLinkNodes(bpy.types.Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def save_layer_cache(layer_obj, output_dir):
|
||||
pattern = r'\.\d{4}\.'
|
||||
|
||||
# "//"
|
||||
output_dir = bpy.path.abspath(output_dir)
|
||||
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = re.search(pattern, source_path.name) is not None
|
||||
name = layer_obj.name if is_sequence else f"{layer_obj.name}.vdb"
|
||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
||||
if is_sequence else Path(output_dir, name).resolve()
|
||||
|
||||
if output_path != source_path:
|
||||
copy_to_dir(source_path.parent if is_sequence else source_path,
|
||||
output_path.parent.parent if is_sequence else output_path.parent,
|
||||
new_name=name)
|
||||
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
layer_obj.data.filepath = bpy.path.relpath(
|
||||
str(output_path), start=str(blend_path))
|
||||
|
||||
|
||||
class SaveStagedData(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_staged_data"
|
||||
bl_label = "Save Staged Data"
|
||||
@@ -114,17 +87,19 @@ class SaveStagedData(bpy.types.Operator):
|
||||
|
||||
if self.save_cache:
|
||||
fails = []
|
||||
for layer in get_all_layer_objs():
|
||||
for layer_obj in get_all_layer_objs():
|
||||
try:
|
||||
save_layer_cache(layer, self.cache_dir)
|
||||
bpy.ops.bioxelnodes.save_layer_cache('EXEC_DEFAULT',
|
||||
layer_obj_name=layer_obj.name,
|
||||
cache_dir=self.cache_dir)
|
||||
except:
|
||||
fails.append(layer)
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} fail to save.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -147,51 +122,6 @@ class SaveStagedData(bpy.types.Operator):
|
||||
panel.prop(self, "lib_dir")
|
||||
|
||||
|
||||
class SaveCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_caches"
|
||||
bl_label = "Save Caches"
|
||||
bl_description = "Save Container's caches to directory."
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Layer Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
return len(container_objs) > 0
|
||||
|
||||
def execute(self, context):
|
||||
container_objs = get_container_objs_from_selection()
|
||||
|
||||
if len(container_objs) == 0:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
fails = []
|
||||
for container_obj in container_objs:
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
try:
|
||||
save_layer_cache(layer_obj, self.cache_dir)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
width=500)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class CleanAllCaches(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_all_caches"
|
||||
bl_label = "Clean All Caches in Temp"
|
||||
@@ -213,3 +143,71 @@ class CleanAllCaches(bpy.types.Operator):
|
||||
event,
|
||||
message="All caches will be cleaned, include other project files, 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"
|
||||
|
||||
PRESETS = {
|
||||
"slice_viewer": "Slice Viewer",
|
||||
"eevee_preview": "EEVEE Preview",
|
||||
"eevee_production": "EEVEE Production",
|
||||
"cycles_preview": "Cycles Preview",
|
||||
"cycles_production": "Cycles Production"
|
||||
}
|
||||
|
||||
preset: bpy.props.EnumProperty(name="Preset",
|
||||
default="eevee_preview",
|
||||
items=[(k, v, "")
|
||||
for k, v in PRESETS.items()]) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.preset == "eevee_preview":
|
||||
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.taa_samples = 16
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 16
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
elif self.preset == "eevee_production":
|
||||
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.taa_samples = 16
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '1'
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 256
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 16
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
elif self.preset == "cycles_preview":
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
bpy.context.scene.cycles.shading_system = True
|
||||
bpy.context.scene.cycles.volume_bounces = 12
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||
bpy.context.scene.cycles.volume_step_rate = 10
|
||||
|
||||
elif self.preset == "cycles_production":
|
||||
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 = 1
|
||||
bpy.context.scene.cycles.volume_step_rate = 1
|
||||
|
||||
elif self.preset == "slice_viewer":
|
||||
# bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.taa_samples = 4
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -1,31 +1,10 @@
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
|
||||
from ..bioxelutils.utils import get_layer_prop_value
|
||||
from .. import __package__ as base_package
|
||||
|
||||
|
||||
def change_render_setting(context):
|
||||
preferences = get_preferences(context)
|
||||
if preferences.do_change_render_setting:
|
||||
bpy.context.scene.render.engine = 'CYCLES'
|
||||
try:
|
||||
bpy.context.scene.cycles.shading_system = True
|
||||
bpy.context.scene.cycles.volume_bounces = 12
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||
bpy.context.scene.cycles.volume_step_rate = 10
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_samples = 256
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def select_object(target_obj):
|
||||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
@@ -58,3 +37,24 @@ def get_cache_dir(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}"
|
||||
|
||||
Reference in New Issue
Block a user