feat: Add Bioxel asset catalog and layer management functionality
- Introduced a new asset catalog definition file for Blender to manage Bioxel assets. - Implemented layer caching and snapshot functionality in `layer.py`, allowing for VDB file generation and low-resolution snapshots. - Created node management functions in `node.py` for handling Bioxel nodes within Blender's Geometry Node Editor. - Added an operator to extract mesh data from selected nodes in `structure.py`. - Developed UI panels in `panels.py` for layer management, including import options and layer library display.
This commit is contained in:
@@ -1,649 +0,0 @@
|
||||
import bpy
|
||||
|
||||
|
||||
import bmesh
|
||||
|
||||
from ..constants import COMPONENT_OUTPUT_NODES
|
||||
from ..utils import get_cache_dir, get_use_link, select_object
|
||||
from ..bioxel.io import load_container, save_container
|
||||
from ..bioxelutils.container import container_to_obj, obj_to_container
|
||||
from ..bioxelutils.node import add_node_to_graph
|
||||
from ..bioxelutils.common import (get_container_layer_objs, get_container_obj, get_node_type,
|
||||
get_nodes_by_type, get_output_node, is_missing_layer,
|
||||
move_node_between_nodes,
|
||||
move_node_to_node,
|
||||
get_all_layer_objs)
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
|
||||
class SaveContainer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_container"
|
||||
bl_label = "Save Container (.bioxel) (BETA)"
|
||||
bl_description = "Clean all caches saved in temp"
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
subtype="FILE_PATH"
|
||||
) # type: ignore
|
||||
|
||||
filename_ext = ".bioxel"
|
||||
|
||||
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'}
|
||||
|
||||
container = obj_to_container(container_obj)
|
||||
save_path = f"{self.filepath.split('.')[0]}.bioxel"
|
||||
save_container(container, save_path, overwrite=True)
|
||||
|
||||
self.report({"INFO"}, f"Successfully save to {save_path}")
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class LoadContainer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.load_container"
|
||||
bl_label = "Load Container (.bioxel) (BETA)"
|
||||
bl_description = "Clean all caches saved in temp"
|
||||
|
||||
filepath: bpy.props.StringProperty(
|
||||
subtype="FILE_PATH"
|
||||
) # type: ignore
|
||||
|
||||
filename_ext = ".bioxel"
|
||||
|
||||
def execute(self, context):
|
||||
load_path = self.filepath
|
||||
container = load_container(self.filepath)
|
||||
is_first_import = len(get_all_layer_objs()) == 0
|
||||
|
||||
container_obj = container_to_obj(container,
|
||||
scene_scale=0.01,
|
||||
cache_dir=get_cache_dir())
|
||||
select_object(container_obj)
|
||||
|
||||
if is_first_import:
|
||||
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||
preset="preview_c")
|
||||
|
||||
self.report({"INFO"}, f"Successfully load {load_path}")
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
class ExtractNodeObject():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
# nodes = [node for node in context.selected_nodes
|
||||
# if get_node_type(node).removeprefix("BioxelNodes_") in COMPONENT_OUTPUT_NODES]
|
||||
|
||||
if len(context.selected_nodes) == 0:
|
||||
self.report({"WARNING"}, "No node selected.")
|
||||
return {'FINISHED'}
|
||||
|
||||
selected_node = context.selected_nodes[0]
|
||||
container_node_group = container_obj.modifiers[0].node_group
|
||||
|
||||
container_output_node = get_output_node(container_node_group)
|
||||
|
||||
if len(container_output_node.inputs[0].links) == 0:
|
||||
pre_socket = None
|
||||
else:
|
||||
pre_socket = container_output_node.inputs[0].links[0].from_socket
|
||||
|
||||
container_node_group.links.new(selected_node.outputs[0],
|
||||
container_output_node.inputs[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
|
||||
|
||||
obj.name = f"{container_obj.name}_{self.object_type}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||
fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
|
||||
node_group,
|
||||
node_label=f"Fetch {self.object_type}",
|
||||
use_link=get_use_link())
|
||||
|
||||
fetch_mesh_node.inputs[0].default_value = container_obj
|
||||
node_group.links.new(fetch_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
for modifier in obj.modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
|
||||
obj.data.materials.clear()
|
||||
|
||||
if pre_socket:
|
||||
container_node_group.links.new(pre_socket,
|
||||
container_output_node.inputs[0])
|
||||
|
||||
select_object(obj)
|
||||
|
||||
self.report({"INFO"}, f"Successfully picked")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ExtractNodeMesh(bpy.types.Operator, ExtractNodeObject):
|
||||
bl_idname = "bioxelnodes.extract_node_mesh"
|
||||
bl_label = "Extract Mesh"
|
||||
bl_description = "Extract Mesh"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
object_type = "Mesh"
|
||||
|
||||
|
||||
class ExtractNodeShapeWire(bpy.types.Operator, ExtractNodeObject):
|
||||
bl_idname = "bioxelnodes.extract_node_shape_wire"
|
||||
bl_label = "Extract Shape Wire"
|
||||
bl_description = "Extract Shape Wire"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
object_type = "ShapeWire"
|
||||
|
||||
|
||||
class ExtractNodeBboxWire(bpy.types.Operator, ExtractNodeObject):
|
||||
bl_idname = "bioxelnodes.extract_node_bbox_wire"
|
||||
bl_label = "Extract Bbox Wire"
|
||||
bl_description = "Extract Bbox Wire"
|
||||
bl_icon = "MESH_CUBE"
|
||||
object_type = "BboxWire"
|
||||
|
||||
|
||||
class ExtractObject():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
obj = bpy.context.active_object
|
||||
|
||||
obj.name = f"{container_obj.name}_{self.object_type}"
|
||||
|
||||
bpy.ops.node.new_geometry_nodes_modifier()
|
||||
modifier = obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
output_node = get_output_node(node_group)
|
||||
|
||||
fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
|
||||
node_group,
|
||||
node_label=f"Fetch {self.object_type}",
|
||||
use_link=get_use_link())
|
||||
|
||||
fetch_mesh_node.inputs[0].default_value = container_obj
|
||||
|
||||
if self.object_type == "Mesh":
|
||||
node_group.links.new(
|
||||
fetch_mesh_node.outputs[0], output_node.inputs[0])
|
||||
else:
|
||||
curve_to_mesh_node = node_group.nodes.new(
|
||||
"GeometryNodeCurveToMesh")
|
||||
node_group.links.new(
|
||||
fetch_mesh_node.outputs[0], curve_to_mesh_node.inputs[0])
|
||||
node_group.links.new(
|
||||
curve_to_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
for modifier in obj.modifiers:
|
||||
bpy.ops.object.modifier_apply(modifier=modifier.name)
|
||||
|
||||
obj.data.materials.clear()
|
||||
|
||||
select_object(obj)
|
||||
|
||||
self.report({"INFO"}, f"Successfully picked")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ExtractMesh(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.extract_mesh"
|
||||
bl_label = "Extract Mesh"
|
||||
bl_description = "Extract Mesh"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
object_type = "Mesh"
|
||||
|
||||
|
||||
class ExtractShapeWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.extract_shape_wire"
|
||||
bl_label = "Extract Shape Wire"
|
||||
bl_description = "Extract Shape Wire"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
object_type = "ShapeWire"
|
||||
|
||||
|
||||
class ExtractBboxWire(bpy.types.Operator, ExtractObject):
|
||||
bl_idname = "bioxelnodes.extract_bbox_wire"
|
||||
bl_label = "Extract Bbox Wire"
|
||||
bl_description = "Extract Bbox Wire"
|
||||
bl_icon = "MESH_CUBE"
|
||||
object_type = "BboxWire"
|
||||
|
||||
|
||||
class AddCutter():
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
# 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.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.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.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.cutter_type == "pie":
|
||||
bpy.ops.mesh.primitive_plane_add(
|
||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
||||
pie_obj = bpy.context.active_object
|
||||
orig_data = pie_obj.data
|
||||
|
||||
# Create mesh
|
||||
pie_mesh = bpy.data.meshes.new('Pie')
|
||||
pie_obj.data = pie_mesh
|
||||
bpy.data.meshes.remove(orig_data)
|
||||
|
||||
# # 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_obj = bpy.context.active_object
|
||||
|
||||
# vcos = [container_obj.matrix_world @
|
||||
# v.co for v in container_obj.data.vertices]
|
||||
|
||||
# def find_center(l): return (max(l) + min(l)) / 2
|
||||
|
||||
# x, y, z = [[v[i] for v in vcos] for i in range(3)]
|
||||
# center = [find_center(axis) for axis in [x, y, z]]
|
||||
|
||||
# cutter_obj.location = center
|
||||
name = f"{self.cutter_type.capitalize()}_Cutter"
|
||||
cutter_obj.name = name
|
||||
cutter_obj.data.name = name
|
||||
cutter_obj.visible_camera = False
|
||||
cutter_obj.visible_diffuse = False
|
||||
cutter_obj.visible_glossy = False
|
||||
cutter_obj.visible_transmission = False
|
||||
cutter_obj.visible_volume_scatter = False
|
||||
cutter_obj.visible_shadow = False
|
||||
cutter_obj.hide_render = True
|
||||
cutter_obj.display_type = 'WIRE'
|
||||
cutter_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
cut_nodes = get_nodes_by_type(node_group,
|
||||
'BioxelNodes_Cut')
|
||||
if len(cut_nodes) == 0:
|
||||
cutter_node = add_node_to_graph("ObjectCutter",
|
||||
node_group,
|
||||
node_label=f"{self.cutter_type.capitalize()} Cutter",
|
||||
use_link=get_use_link())
|
||||
cutter_node.inputs[0].default_value = self.cutter_type.capitalize()
|
||||
cutter_node.inputs[1].default_value = cutter_obj
|
||||
|
||||
cut_node = add_node_to_graph("Cut",
|
||||
node_group,
|
||||
use_link=get_use_link())
|
||||
|
||||
output_node = get_output_node(node_group)
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(cut_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(cut_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
cut_node.inputs[0])
|
||||
node_group.links.new(cut_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(cut_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
node_group.links.new(cutter_node.outputs[0],
|
||||
cut_node.inputs[1])
|
||||
|
||||
move_node_to_node(cutter_node, cut_node, (-300, -300))
|
||||
else:
|
||||
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||
node_name="ObjectCutter",
|
||||
node_label=name)
|
||||
node = bpy.context.active_node
|
||||
node.inputs[0].default_value = self.cutter_type.capitalize()
|
||||
node.inputs[1].default_value = cutter_obj
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
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 current container"
|
||||
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 current container"
|
||||
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 current container"
|
||||
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 current container"
|
||||
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 current container"
|
||||
bl_icon = "MESH_CONE"
|
||||
cutter_type = "pie"
|
||||
|
||||
|
||||
class AddSlicer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_slicer"
|
||||
bl_label = "Add a Slicer"
|
||||
bl_description = "Add a slicer to current container"
|
||||
bl_icon = "TEXTURE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.ops.mesh.primitive_plane_add(size=2,
|
||||
enter_editmode=False,
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
slicer_obj = bpy.context.active_object
|
||||
slicer_obj.name = "Slicer"
|
||||
slicer_obj.data.name = "Slicer"
|
||||
|
||||
slicer_obj.visible_camera = False
|
||||
slicer_obj.visible_diffuse = False
|
||||
slicer_obj.visible_glossy = False
|
||||
slicer_obj.visible_transmission = False
|
||||
slicer_obj.visible_volume_scatter = False
|
||||
slicer_obj.visible_shadow = False
|
||||
slicer_obj.hide_render = True
|
||||
slicer_obj.display_type = 'WIRE'
|
||||
slicer_obj.lineart.usage = 'EXCLUDE'
|
||||
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
|
||||
slice_node = add_node_to_graph("Slice",
|
||||
node_group,
|
||||
use_link=get_use_link())
|
||||
|
||||
slice_node.inputs[1].default_value = slicer_obj
|
||||
|
||||
output_node = get_output_node(node_group)
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(slice_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
slice_node.inputs[0])
|
||||
node_group.links.new(slice_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(slice_node,
|
||||
[pre_output_node, output_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddLocator(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_locator"
|
||||
bl_label = "Add a Locator"
|
||||
bl_description = "Add a locator to current container"
|
||||
bl_icon = "EMPTY_AXIS"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
bpy.ops.object.empty_add(type='ARROWS',
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
locator_obj = bpy.context.active_object
|
||||
locator_obj.name = "Locator"
|
||||
select_object(container_obj)
|
||||
|
||||
modifier = container_obj.modifiers[0]
|
||||
node_group = modifier.node_group
|
||||
try:
|
||||
selected_node = context.selected_nodes[0]
|
||||
except:
|
||||
selected_node = None
|
||||
|
||||
parent_node = add_node_to_graph("TransformParent",
|
||||
node_group,
|
||||
node_label="Transform Parent",
|
||||
use_link=get_use_link())
|
||||
|
||||
parent_node.inputs[1].default_value = locator_obj
|
||||
|
||||
if selected_node is None or len(selected_node.outputs) == 0:
|
||||
output_node = get_output_node(node_group)
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(parent_node, output_node, (-300, 0))
|
||||
else:
|
||||
pre_output_node = output_node.inputs[0].links[0].from_node
|
||||
node_group.links.new(pre_output_node.outputs[0],
|
||||
parent_node.inputs[0])
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_between_nodes(parent_node,
|
||||
[pre_output_node, output_node])
|
||||
else:
|
||||
try:
|
||||
to_node = selected_node.outputs[0].links[0].to_node
|
||||
except:
|
||||
to_node = None
|
||||
|
||||
if to_node is None:
|
||||
node_group.links.new(selected_node.outputs[0],
|
||||
parent_node.inputs[0])
|
||||
move_node_to_node(parent_node, selected_node, (300, 0))
|
||||
else:
|
||||
node_group.links.new(selected_node.outputs[0],
|
||||
parent_node.inputs[0])
|
||||
node_group.links.new(parent_node.outputs[0],
|
||||
to_node.inputs[0])
|
||||
move_node_between_nodes(parent_node,
|
||||
[selected_node, to_node])
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ContainerProps(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.container_props"
|
||||
bl_label = "Change Container Properties"
|
||||
bl_description = "Change current ontainer properties"
|
||||
bl_icon = "FILE_TICK"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
scene_scale: bpy.props.FloatProperty(name="Scene Scale",
|
||||
soft_min=0.0001, soft_max=10.0,
|
||||
min=1e-6, max=1e6,
|
||||
default=0.01) # type: ignore
|
||||
|
||||
step_size: bpy.props.FloatProperty(name="Step Size",
|
||||
soft_min=0.1, soft_max=100.0,
|
||||
min=0.1, max=1e2,
|
||||
default=1) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {'FINISHED'}
|
||||
|
||||
container_obj.scale[0] = self.scene_scale
|
||||
container_obj.scale[1] = self.scene_scale
|
||||
container_obj.scale[2] = self.scene_scale
|
||||
container_obj["scene_scale"] = self.scene_scale
|
||||
container_obj["step_size"] = self.step_size
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
layer_obj.data.render.space = 'WORLD'
|
||||
layer_obj.data.render.step_size = self.scene_scale * self.step_size
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
container_obj = get_container_obj(context.object)
|
||||
|
||||
if container_obj is None:
|
||||
return self.execute(context)
|
||||
else:
|
||||
self.scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
self.step_size = container_obj.get("step_size") or 1
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
|
||||
def get_container_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
container_obj = context.object
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
return [obj for obj in layer_objs if layer_filter(obj, context)]
|
||||
|
||||
|
||||
class SaveContainerLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_container_layers_cache"
|
||||
bl_label = "Save Container Layers' Cache"
|
||||
bl_description = "Save all current container layers' cache to directory."
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all container layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_not_missing)
|
||||
|
||||
|
||||
class RemoveContainerMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_container_missing_layers"
|
||||
bl_label = "Remove Container Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all container missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj, context):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_container_layers(context, is_missing)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
return {'RUNNING_MODAL'}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,619 +1,320 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import bpy
|
||||
|
||||
import numpy as np
|
||||
from ..node import add_bioxel_node, get_layer_nodes, get_main_node_group
|
||||
from ..utils import refresh_bioxel_panels
|
||||
from ..layer import get_layer_caches, set_layer_caches
|
||||
|
||||
|
||||
from ..exceptions import NoContent
|
||||
from ..bioxel.layer import Layer
|
||||
from ..bioxelutils.node import add_node_to_graph
|
||||
from ..bioxelutils.common import (get_container_obj, get_layer_kind, get_layer_label, get_layer_name,
|
||||
get_layer_prop_value,
|
||||
get_container_layer_objs,
|
||||
get_node_type, is_missing_layer, move_node_to_node, set_layer_prop_value)
|
||||
from ..bioxelutils.layer import layer_to_obj, obj_to_layer
|
||||
from ..utils import get_cache_dir, copy_to_dir, get_use_link
|
||||
class RenameLayer(bpy.types.Operator):
|
||||
"""Rename a cached Bioxel layer"""
|
||||
|
||||
bl_idname = "bioxel.rename_layer"
|
||||
bl_label = "Rename Layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def get_label_layer_selection(self, context):
|
||||
items = [("None", "None", "")]
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore
|
||||
new_name: bpy.props.StringProperty(
|
||||
name="New name", default="") # type: ignore
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
kind = get_layer_prop_value(layer_obj, "kind")
|
||||
name = get_layer_prop_value(layer_obj, "name")
|
||||
if kind == "label":
|
||||
items.append((layer_obj.name,
|
||||
name,
|
||||
""))
|
||||
|
||||
return items
|
||||
|
||||
|
||||
class FetchLayer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.fetch_layer"
|
||||
bl_label = "Fetch Layer"
|
||||
bl_description = "Fetch layer from current container"
|
||||
bl_icon = "NODE"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
layer_obj_name: bpy.props.StringProperty(
|
||||
options={"HIDDEN"}) # type: ignore
|
||||
|
||||
@classmethod
|
||||
def description(cls, context, properties):
|
||||
layer_obj_name = properties.layer_obj_name
|
||||
layer_obj = bpy.data.objects.get(layer_obj_name)
|
||||
return "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
|
||||
for prop in ["kind",
|
||||
"bioxel_size",
|
||||
"shape",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
@property
|
||||
def layer_obj(self):
|
||||
return bpy.data.objects.get(self.layer_obj_name)
|
||||
def invoke(self, context, event):
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id", "")) == self.cache_id), None)
|
||||
if entry:
|
||||
self.new_name = str(entry.get("name", ""))
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
if self.layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
if not self.new_name:
|
||||
self.report({"ERROR"}, "Name cannot be empty")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if is_missing_layer(self.layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
caches = get_layer_caches()
|
||||
for c in caches:
|
||||
if str(c.get("id", "")) == self.cache_id:
|
||||
c["name"] = self.new_name
|
||||
break
|
||||
|
||||
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||
node_name="FetchLayer",
|
||||
node_label="Fetch Layer")
|
||||
node = bpy.context.active_node
|
||||
try:
|
||||
set_layer_caches(caches)
|
||||
refresh_bioxel_panels(context)
|
||||
self.report({"INFO"}, "Layer renamed")
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to save changes: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
node.inputs[0].default_value = self.layer_obj
|
||||
node.label = get_layer_label(self.layer_obj)
|
||||
|
||||
class DeleteLayer(bpy.types.Operator):
|
||||
"""Delete a saved Bioxel layer"""
|
||||
|
||||
bl_idname = "bioxel.delete_layer"
|
||||
bl_label = "Delete Layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
cache_id: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
def execute(self, context):
|
||||
if not self.cache_id:
|
||||
self.report({"ERROR"}, "Missing cache id")
|
||||
return {"CANCELLED"}
|
||||
|
||||
caches = get_layer_caches()
|
||||
new_list = [c for c in caches if str(c.get("id", "")) != self.cache_id]
|
||||
removed = next(
|
||||
(c for c in caches if str(c.get("id", "")) == self.cache_id), None
|
||||
)
|
||||
|
||||
try:
|
||||
set_layer_caches(new_list)
|
||||
refresh_bioxel_panels(context)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to update layer list: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.report({"INFO"}, "Layer removed")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def get_selected_layers(context, layer_filter=None):
|
||||
def _layer_filter(layer_obj, context):
|
||||
return True
|
||||
# 添加图层到节点图的操作器
|
||||
class AddLayerNode(bpy.types.Operator):
|
||||
"""将选中的图层添加到几何节点图"""
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
select_objs = []
|
||||
# node_group = context.space_data.edit_tree
|
||||
for node in context.selected_nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
layer_obj = node.inputs[0].default_value
|
||||
if layer_obj is not None:
|
||||
if layer_filter(layer_obj, context):
|
||||
select_objs.append(layer_obj)
|
||||
bl_idname = "bioxel.add_layer_node"
|
||||
bl_label = "添加图层节点"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
return list(set(select_objs))
|
||||
|
||||
|
||||
def get_selected_layer(context, layer_filter=None):
|
||||
layer_objs = get_selected_layers(context, layer_filter)
|
||||
return layer_objs[0] if len(layer_objs) > 0 else None
|
||||
|
||||
|
||||
class OutputLayerOperator():
|
||||
new_layer_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
"""do the operation"""
|
||||
return orig_layer
|
||||
|
||||
def add_layer_node(self, context, layer):
|
||||
orig_node = context.selected_nodes[0]
|
||||
layer_obj = layer_to_obj(layer,
|
||||
container_obj=context.object,
|
||||
cache_dir=get_cache_dir())
|
||||
node_group = context.space_data.edit_tree
|
||||
fetch_node = add_node_to_graph("FetchLayer",
|
||||
node_group,
|
||||
use_link=get_use_link())
|
||||
fetch_node.label = get_layer_prop_value(layer_obj, "name")
|
||||
fetch_node.inputs[0].default_value = layer_obj
|
||||
move_node_to_node(fetch_node, orig_node, (0, -100))
|
||||
cache_id: bpy.props.StringProperty() # type: ignore # 图层ID
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
# 获取图层数据
|
||||
caches = get_layer_caches()
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
# 查找目标图层
|
||||
entry = next(
|
||||
(c for c in caches if str(c.get("id", "")) == str(self.cache_id)), None
|
||||
)
|
||||
|
||||
orig_layer = obj_to_layer(layer_obj)
|
||||
if not entry:
|
||||
self.report({"ERROR"}, "Layer not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 创建并设置节点属性
|
||||
try:
|
||||
new_layer = self.operate(orig_layer, context)
|
||||
except NoContent as e:
|
||||
self.report({"WARNING"}, e.message)
|
||||
return {'FINISHED'}
|
||||
layer_node = add_bioxel_node("O Layer")
|
||||
except:
|
||||
self.report({"ERROR"}, "Fail to add layer node")
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
layer_node.node_tree.make_local()
|
||||
layer_node.inputs["Path"].default_value = entry["path"]
|
||||
layer_node.inputs["Name"].default_value = entry["name"]
|
||||
layer_node.inputs["Shape"].default_value = entry["shape"]
|
||||
layer_node.inputs["Min"].default_value = entry["min"]
|
||||
layer_node.inputs["Max"].default_value = entry["max"]
|
||||
layer_node.inputs["ID"].default_value = entry["id"]
|
||||
|
||||
# 将特定属性隐藏到hidden面板
|
||||
hidden_sockets = ["Path", "Shape", "Min", "Max", "ID", "Animation"]
|
||||
|
||||
class LayerOperator():
|
||||
|
||||
def operate(self, layer_obj: bpy.types.Object, context):
|
||||
"""do the operation"""
|
||||
...
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
self.report({"WARNING"}, "Selected layer is lost.")
|
||||
return {'FINISHED'}
|
||||
|
||||
self.operate(layer_obj, context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RetimeLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.retime_layer"
|
||||
bl_label = "Retime Sequence"
|
||||
bl_description = "Retime layer time sequence"
|
||||
bl_icon = "TIME"
|
||||
|
||||
frame_duration: bpy.props.IntProperty(name="Frames") # type: ignore
|
||||
frame_start: bpy.props.IntProperty(name="Start") # type: ignore
|
||||
frame_offset: bpy.props.IntProperty(name="Offset") # type: ignore
|
||||
sequence_mode: bpy.props.EnumProperty(name="Mode",
|
||||
default="REPEAT",
|
||||
items=[("CLIP", "Clip", ""),
|
||||
("EXTEND", "Extend", ""),
|
||||
("REPEAT", "Repeat", ""),
|
||||
("PING_PONG", "Ping-Pong", "")]) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.frame_duration = self.frame_duration
|
||||
layer_obj.data.frame_start = self.frame_start
|
||||
layer_obj.data.frame_offset = self.frame_offset
|
||||
layer_obj.data.sequence_mode = self.sequence_mode
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.frame_duration = layer_obj.data.frame_duration
|
||||
self.frame_start = layer_obj.data.frame_start
|
||||
self.frame_offset = layer_obj.data.frame_offset
|
||||
self.sequence_mode = layer_obj.data.sequence_mode
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Retime {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
if entry["frame_count"] > 1:
|
||||
layer_node.inputs["Frame Count"].default_value = entry["frame_count"]
|
||||
layer_node.inputs["Animation"].default_value = True
|
||||
else:
|
||||
return self.execute(context)
|
||||
hidden_sockets = hidden_sockets + \
|
||||
["Frame Count", "Frame Offset", "Cycle"]
|
||||
|
||||
# 将特定属性隐藏到hidden面板
|
||||
for socket_name in hidden_sockets:
|
||||
if socket_name in layer_node.inputs:
|
||||
layer_node.inputs[socket_name].hide = True
|
||||
|
||||
for socket_name in layer_node.outputs.keys():
|
||||
if socket_name != entry["kind"].capitalize():
|
||||
layer_node.outputs[socket_name].hide = True
|
||||
|
||||
self.report({"INFO"}, f"Successfully created node: {entry['name']}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class RelocateLayer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relocate_layer"
|
||||
bl_label = "Relocate Layer Cache"
|
||||
bl_description = "Relocate layer cache"
|
||||
bl_icon = "FILE"
|
||||
class RelocatePath(bpy.types.Operator):
|
||||
"""Locate and update the missing layer folder, and update all related nodes"""
|
||||
|
||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||
bl_idname = "bioxel.find_layer_folder"
|
||||
bl_label = "Relocate"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj == None:
|
||||
self.report({"WARNING"}, "Get no layer.")
|
||||
return {'FINISHED'}
|
||||
cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore
|
||||
|
||||
self.operate(layer_obj, context)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
layer_obj.data.filepath = self.filepath
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class RenameLayer(bpy.types.Operator, LayerOperator):
|
||||
bl_idname = "bioxelnodes.rename_layer"
|
||||
bl_label = "Rename Layer"
|
||||
bl_description = "Rename layer"
|
||||
bl_icon = "FILE_FONT"
|
||||
|
||||
new_name: bpy.props.StringProperty(name="New Name",
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
name = f"{layer_obj.parent.name}_{self.new_name}"
|
||||
layer_obj.name = name
|
||||
layer_obj.data.name = name
|
||||
|
||||
set_layer_prop_value(layer_obj, "name", self.new_name)
|
||||
|
||||
node_group = context.space_data.edit_tree
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node.label = self.new_name
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
self.new_name = get_layer_name(layer_obj)
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Rename {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class ResampleLayer(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.resample_layer"
|
||||
bl_label = "Resample Value"
|
||||
bl_description = "Resample value"
|
||||
bl_icon = "ALIASED"
|
||||
|
||||
smooth: bpy.props.IntProperty(name="Smooth Size",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
bioxel_size: bpy.props.FloatProperty(
|
||||
name="Bioxel Size",
|
||||
soft_min=0.1, soft_max=10.0,
|
||||
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 operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_shape = self.get_new_shape(new_layer.shape,
|
||||
new_layer.bioxel_size[0],
|
||||
self.bioxel_size)
|
||||
|
||||
new_layer.resize(new_shape, self.smooth)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_R-{self.bioxel_size:.2f}"
|
||||
return new_layer
|
||||
|
||||
def draw(self, context):
|
||||
layer_obj = get_selected_layer(context)
|
||||
orig_shape = get_layer_prop_value(layer_obj, "shape")
|
||||
orig_size = get_layer_prop_value(layer_obj, "bioxel_size")
|
||||
new_shape = self.get_new_shape(orig_shape,
|
||||
orig_size,
|
||||
self.bioxel_size)
|
||||
|
||||
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, "new_layer_name")
|
||||
layout.prop(self, "bioxel_size")
|
||||
layout.prop(self, "smooth")
|
||||
layout.label(text=layer_shape_text)
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
self.bioxel_size = get_layer_prop_value(layer_obj,
|
||||
"bioxel_size")
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Resample {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class SignScalar(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.sign_scalar"
|
||||
bl_label = "Sign Value"
|
||||
bl_description = "Sign value"
|
||||
bl_icon = "REMOVE"
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.data = -orig_layer.data
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_Sign"
|
||||
return new_layer
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Sign {name}")
|
||||
return {'RUNNING_MODAL'}
|
||||
else:
|
||||
return self.execute(context)
|
||||
|
||||
|
||||
class FillOperator(OutputLayerOperator):
|
||||
|
||||
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 Aera",
|
||||
use_relative: bpy.props.BoolProperty(
|
||||
name="Relative Path",
|
||||
description="Store the path relative to the current Blender file",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
def invoke(self, context, event):
|
||||
layer_obj = get_selected_layer(context)
|
||||
if layer_obj:
|
||||
name = get_layer_label(layer_obj)
|
||||
context.window_manager.invoke_props_dialog(self,
|
||||
title=f"Fill {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,
|
||||
directory: bpy.props.StringProperty(
|
||||
name="Layer Folder",
|
||||
description="Select the correct folder for this layer",
|
||||
subtype="DIR_PATH",
|
||||
) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = data <= self.threshold \
|
||||
if self.invert else data > self.threshold
|
||||
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 0)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.threshold:.2f}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class FillByRange(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_range"
|
||||
bl_label = "Fill Value by Range"
|
||||
bl_description = "Fill value by range"
|
||||
bl_icon = "IPO_CONSTANT"
|
||||
|
||||
from_min: bpy.props.FloatProperty(
|
||||
name="From Min",
|
||||
soft_min=0, soft_max=1024,
|
||||
default=128,
|
||||
) # type: ignore
|
||||
|
||||
from_max: bpy.props.FloatProperty(
|
||||
name="From Max",
|
||||
soft_min=0, soft_max=1024,
|
||||
default=256,
|
||||
) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
data = np.amax(orig_layer.data, -1)
|
||||
mask = (data <= self.from_min) | (data >= self.from_max) if self.invert else \
|
||||
(data > self.from_min) & (data < self.from_max)
|
||||
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 0)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{self.from_min:.2f}-{self.from_max:.2f}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class FillByLabel(bpy.types.Operator, FillOperator):
|
||||
bl_idname = "bioxelnodes.fill_by_label"
|
||||
bl_label = "Fill Value by Label"
|
||||
bl_description = "Fill value by label"
|
||||
bl_icon = "MESH_CAPSULE"
|
||||
|
||||
smooth: bpy.props.IntProperty(name="Smooth Size",
|
||||
default=0,
|
||||
soft_min=0, soft_max=5,
|
||||
options={"SKIP_SAVE"}) # type: ignore
|
||||
|
||||
label_obj_name: bpy.props.EnumProperty(name="Label Layer",
|
||||
items=get_label_layer_selection) # type: ignore
|
||||
|
||||
def operate(self, orig_layer: Layer, context):
|
||||
label_obj = bpy.data.objects.get(self.label_obj_name)
|
||||
if label_obj is None:
|
||||
raise NoContent("Cannot find any label layer.")
|
||||
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(orig_layer.shape, self.smooth)
|
||||
mask = np.amax(label_layer.data, -1)
|
||||
if self.invert:
|
||||
mask = 1 - mask
|
||||
|
||||
new_layer = orig_layer.copy()
|
||||
new_layer.fill(self.fill_value, mask, 0)
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"{orig_layer.name}_F-{label_layer.name}"
|
||||
return new_layer
|
||||
|
||||
|
||||
class CombineLabels(bpy.types.Operator, OutputLayerOperator):
|
||||
bl_idname = "bioxelnodes.combine_labels"
|
||||
bl_label = "Combine Labels"
|
||||
bl_description = "Combine all selected labels"
|
||||
bl_icon = "MOD_BUILD"
|
||||
|
||||
def execute(self, context):
|
||||
def layer_filter(layer_obj, context):
|
||||
return get_layer_kind(layer_obj) == "label"
|
||||
|
||||
label_objs = get_selected_layers(context, layer_filter)
|
||||
|
||||
if len(label_objs) < 2:
|
||||
self.report({"WARNING"}, "Not enough layers.")
|
||||
return {'FINISHED'}
|
||||
|
||||
base_obj = label_objs[0]
|
||||
label_objs = label_objs[1:]
|
||||
|
||||
base_layer = obj_to_layer(base_obj)
|
||||
new_layer = base_layer.copy()
|
||||
label_names = [base_layer.name]
|
||||
|
||||
for label_obj in label_objs:
|
||||
label_layer = obj_to_layer(label_obj)
|
||||
label_layer.resize(base_layer.shape)
|
||||
new_layer.data = np.maximum(
|
||||
new_layer.data, label_layer.data)
|
||||
label_names.append(label_layer.name)
|
||||
|
||||
new_layer.name = self.new_layer_name \
|
||||
or f"C-{'-'.join(label_names)}"
|
||||
|
||||
self.add_layer_node(context, new_layer)
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class BatchLayerOperator():
|
||||
success_msg = "Successfully done selected layers."
|
||||
fail_msg = "fails."
|
||||
|
||||
def execute(self, context):
|
||||
layer_objs = self.get_layers(context)
|
||||
|
||||
fails = []
|
||||
for layer_obj in layer_objs:
|
||||
try:
|
||||
self.operate(layer_obj, context)
|
||||
except:
|
||||
fails.append(layer_obj)
|
||||
|
||||
if len(fails) == 0:
|
||||
self.report({"INFO"}, self.success_msg)
|
||||
else:
|
||||
self.report(
|
||||
{"WARNING"}, f"{','.join([layer_obj.name for layer_obj in fails])} {self.fail_msg}")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveLayersCache(BatchLayerOperator):
|
||||
fail_msg = "fail to save."
|
||||
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
output_dir = bpy.path.abspath(self.cache_dir)
|
||||
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||
|
||||
source_path: Path = Path(source_dir).resolve()
|
||||
is_sequence = layer_obj.data.is_sequence
|
||||
|
||||
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))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
if not self.cache_id:
|
||||
self.report({"ERROR"}, "Missing cache id")
|
||||
return {"CANCELLED"}
|
||||
if not self.directory:
|
||||
self.report({"ERROR"}, "Please select a folder")
|
||||
return {"CANCELLED"}
|
||||
|
||||
class RemoveLayers(BatchLayerOperator):
|
||||
fail_msg = "fail to remove."
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id", "")) == self.cache_id), None)
|
||||
if not entry:
|
||||
self.report({"ERROR"}, "Layer not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def operate(self, layer_obj, context):
|
||||
old_path = entry["path"]
|
||||
new_path = (
|
||||
bpy.path.relpath(self.directory)
|
||||
if self.use_relative and bpy.data.filepath
|
||||
else bpy.path.abspath(self.directory)
|
||||
)
|
||||
entry["path"] = new_path
|
||||
|
||||
set_layer_caches(caches)
|
||||
refresh_bioxel_panels(context)
|
||||
|
||||
# 更新所有 node group 中 O Layer 节点的 Path
|
||||
for node_group in bpy.data.node_groups:
|
||||
for node in node_group.nodes:
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node_group.nodes.remove(node)
|
||||
for node in get_layer_nodes(node_group):
|
||||
node_path = getattr(node.inputs.get("Path"), "default_value")
|
||||
if node_path and node_path == old_path:
|
||||
node.inputs["Path"].default_value = new_path
|
||||
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
self.report({"INFO"}, f"Cache path updated: {new_path}")
|
||||
return {"FINISHED"}
|
||||
|
||||
if cache_filepath.is_file():
|
||||
if layer_obj.data.is_sequence:
|
||||
for f in cache_filepath.parent.iterdir():
|
||||
f.unlink(missing_ok=True)
|
||||
else:
|
||||
cache_filepath.unlink(missing_ok=True)
|
||||
|
||||
# also remove layer object
|
||||
bpy.data.volumes.remove(layer_obj.data)
|
||||
class SaveLayer(bpy.types.Operator):
|
||||
"""Copy the entire layer folder (named by id) to a target folder and update its path in the cache"""
|
||||
|
||||
return {'FINISHED'}
|
||||
bl_idname = "bioxel.cache_layer_to_folder"
|
||||
bl_label = "Save Layer"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
cache_id: bpy.props.StringProperty(options={"HIDDEN"}) # type: ignore
|
||||
|
||||
use_relative: bpy.props.BoolProperty(
|
||||
name="Relative Path",
|
||||
description="Store the path relative to the current Blender file",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
directory: bpy.props.StringProperty(
|
||||
name="Target Folder",
|
||||
description="Select a folder to cache the layer files",
|
||||
subtype="DIR_PATH",
|
||||
) # type: ignore
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove them?")
|
||||
return {'RUNNING_MODAL'}
|
||||
# Default to Blender file directory if possible
|
||||
blend_dir = Path(
|
||||
bpy.data.filepath).parent if bpy.data.filepath else Path.cwd()
|
||||
context.window_manager.fileselect_add(self)
|
||||
if not self.directory:
|
||||
self.directory = str(blend_dir)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
if not self.cache_id:
|
||||
self.report({"ERROR"}, "Missing cache id")
|
||||
return {"CANCELLED"}
|
||||
if not self.directory:
|
||||
self.report({"ERROR"}, "Please select a target folder")
|
||||
return {"CANCELLED"}
|
||||
|
||||
caches = get_layer_caches()
|
||||
entry = next((c for c in caches if str(
|
||||
c.get("id")) == self.cache_id), None)
|
||||
cache_id = entry.get("id")
|
||||
|
||||
if not entry:
|
||||
self.report({"ERROR"}, "Layer not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
old_path = entry["path"]
|
||||
src_path = bpy.path.abspath(old_path)
|
||||
if not src_path or not Path(src_path).is_dir():
|
||||
self.report({"ERROR"}, "Source folder not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
src_dir = Path(src_path).resolve()
|
||||
dst_root = Path(self.directory)
|
||||
# copy as a subfolder named by id
|
||||
dst_dir = (dst_root / src_dir.name).resolve()
|
||||
|
||||
try:
|
||||
if src_dir != dst_dir:
|
||||
if dst_dir.exists():
|
||||
shutil.rmtree(dst_dir)
|
||||
shutil.copytree(src_dir, dst_dir)
|
||||
|
||||
# Save as relative path if requested and possible
|
||||
|
||||
new_path = (
|
||||
bpy.path.relpath(str(dst_dir))
|
||||
if self.use_relative and bpy.data.filepath
|
||||
else bpy.path.abspath(str(dst_dir))
|
||||
)
|
||||
|
||||
entry["path"] = new_path
|
||||
|
||||
set_layer_caches(caches)
|
||||
refresh_bioxel_panels(context)
|
||||
|
||||
# 遍历所有 node group
|
||||
for node_group in bpy.data.node_groups:
|
||||
for node in get_layer_nodes(node_group):
|
||||
if cache_id == getattr(node.inputs.get("ID"), "default_value"):
|
||||
node.inputs["Path"].default_value = new_path
|
||||
|
||||
self.report({"INFO"}, f"Layer cached to {dst_dir}")
|
||||
return {"FINISHED"}
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to cache layer: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class SaveSelectedLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_selected_layers_cache"
|
||||
bl_label = "Save Selected Layers Cache"
|
||||
bl_description = "Save selected layers' Cache"
|
||||
bl_icon = "FILE_TICK"
|
||||
class SelectAndFocusNode(bpy.types.Operator):
|
||||
"""Select and focus the specified node in the current node tree"""
|
||||
|
||||
success_msg = "Successfully saved all selected layers."
|
||||
bl_idname = "bioxel.select_and_focus_node"
|
||||
bl_label = "Select and Focus Node"
|
||||
node_name: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj, context):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_selected_layers(context, is_not_missing)
|
||||
|
||||
|
||||
class RemoveSelectedLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_selected_layers"
|
||||
bl_label = "Remove Selected Layers"
|
||||
bl_description = "Remove selected layers"
|
||||
bl_icon = "TRASH"
|
||||
|
||||
success_msg = "Successfully removed all selected layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
return get_selected_layers(context)
|
||||
def execute(self, context):
|
||||
node_group = get_main_node_group(context)
|
||||
if not node_group:
|
||||
self.report({"ERROR"}, "No node group found.")
|
||||
return {"CANCELLED"}
|
||||
node = node_group.nodes.get(self.node_name)
|
||||
if not node:
|
||||
self.report({"ERROR"}, f"Node '{self.node_name}' not found.")
|
||||
return {"CANCELLED"}
|
||||
# Deselect all, select and focus this node
|
||||
for n in node_group.nodes:
|
||||
n.select = False
|
||||
node.select = True
|
||||
node_group.nodes.active = node
|
||||
bpy.ops.node.view_selected()
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,110 +1,21 @@
|
||||
import json
|
||||
import uuid
|
||||
import webbrowser
|
||||
import bpy
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
from ..bioxelutils.common import get_all_layer_objs, get_node_lib_path, get_node_version, is_missing_layer, set_file_prop
|
||||
from .layer import RemoveLayers, SaveLayersCache
|
||||
|
||||
from ..constants import LATEST_NODE_LIB_PATH, VERSIONS
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
class ReLinkNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.relink_node_lib"
|
||||
bl_label = "Relink Node Library"
|
||||
bl_description = "Relink all nodes to addon library source"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
index: bpy.props.IntProperty() # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
node_version = VERSIONS[self.index]['node_version']
|
||||
lib_path = get_node_lib_path(node_version) \
|
||||
if self.index != 0 else LATEST_NODE_LIB_PATH
|
||||
|
||||
node_libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
node_lib = node_group.library
|
||||
if node_lib:
|
||||
node_libs.append(node_lib)
|
||||
|
||||
node_libs = list(set(node_libs))
|
||||
|
||||
for node_lib in node_libs:
|
||||
node_lib.filepath = str(lib_path)
|
||||
# FIXME: may cause crash
|
||||
node_lib.reload()
|
||||
|
||||
set_file_prop("node_version", node_version)
|
||||
|
||||
self.report({"INFO"}, f"Successfully relinked.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveNodeLib(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.save_node_lib"
|
||||
bl_label = "Save Node Library"
|
||||
bl_description = "Save node library file to local"
|
||||
bl_options = {'UNDO'}
|
||||
|
||||
lib_dir: bpy.props.StringProperty(
|
||||
name="Library Directory",
|
||||
subtype='DIR_PATH',
|
||||
default="//"
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
node_version = get_node_version()
|
||||
|
||||
if node_version is None:
|
||||
node_version = VERSIONS[0]["node_version"]
|
||||
else:
|
||||
if node_version not in [v["node_version"] for v in VERSIONS]:
|
||||
node_version = VERSIONS[0]["node_version"]
|
||||
|
||||
if node_version == VERSIONS[0]["node_version"]:
|
||||
lib_path = LATEST_NODE_LIB_PATH
|
||||
else:
|
||||
lib_path = get_node_lib_path(node_version)
|
||||
|
||||
version_str = "v"+".".join([str(i) for i in list(node_version)])
|
||||
|
||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||
local_lib_path: Path = Path(lib_dir,
|
||||
f"BioxelNodes_{version_str}.blend").resolve()
|
||||
node_lib_path: Path = lib_path
|
||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||
|
||||
if local_lib_path != node_lib_path:
|
||||
shutil.copy(node_lib_path, local_lib_path)
|
||||
|
||||
libs = []
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
if node_group.library:
|
||||
libs.append(node_group.library)
|
||||
|
||||
libs = list(set(libs))
|
||||
for lib in libs:
|
||||
lib.filepath = bpy.path.relpath(str(local_lib_path),
|
||||
start=str(blend_path))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_props_dialog(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.data.is_saved
|
||||
from ..constants import LATEST_NODE_LIB_PATH
|
||||
from ..utils import (
|
||||
get_all_layer_objs,
|
||||
get_cache_dir,
|
||||
get_node_version,
|
||||
is_missing_layer,
|
||||
)
|
||||
|
||||
|
||||
class CleanTemp(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.clear_temp"
|
||||
bl_idname = "bioxel.clear_temp"
|
||||
bl_label = "Clean Temp"
|
||||
bl_description = "Clean all cache in temp (include other project cache)"
|
||||
|
||||
@@ -113,24 +24,26 @@ class CleanTemp(bpy.types.Operator):
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
self.report({"INFO"}, f"Successfully cleaned temp.")
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
except:
|
||||
self.report({"WARNING"},
|
||||
"Fail to clean temp, you may do it manually.")
|
||||
return {'CANCELLED'}
|
||||
self.report(
|
||||
{"WARNING"}, "Fail to clean temp, you may do it manually.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?")
|
||||
return {'RUNNING_MODAL'}
|
||||
context.window_manager.invoke_confirm(
|
||||
self,
|
||||
event,
|
||||
message="All temp files will be removed, include other project cache, do you still want to clean?",
|
||||
)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
class RenderSettingPreset(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.render_setting_preset"
|
||||
bl_idname = "bioxel.render_setting_preset"
|
||||
bl_label = "Render Setting Presets"
|
||||
bl_description = "Render setting presets for bioxel"
|
||||
bl_options = {'UNDO'}
|
||||
bl_options = {"UNDO"}
|
||||
|
||||
PRESETS = {
|
||||
"performance": "Performance",
|
||||
@@ -138,166 +51,201 @@ class RenderSettingPreset(bpy.types.Operator):
|
||||
"quality": "Quality",
|
||||
}
|
||||
|
||||
preset: bpy.props.EnumProperty(name="Preset",
|
||||
default="balance",
|
||||
items=[(k, v, "")
|
||||
for k, v in PRESETS.items()]) # type: ignore
|
||||
preset: bpy.props.EnumProperty(
|
||||
name="Preset", default="balance", items=[(k, v, "") for k, v in PRESETS.items()]
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context):
|
||||
if self.preset == "performance":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = "4"
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 32
|
||||
bpy.context.scene.eevee.volumetric_samples = 64
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 0
|
||||
bpy.context.scene.cycles.volume_bounces = 4
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 4
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 4
|
||||
bpy.context.scene.cycles.volume_step_rate = 4
|
||||
bpy.context.scene.cycles.volume_biased = True
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 100
|
||||
bpy.context.scene.cycles.volume_step_rate = 100
|
||||
|
||||
elif self.preset == "balance":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = "2"
|
||||
bpy.context.scene.eevee.volumetric_shadow_samples = 64
|
||||
bpy.context.scene.eevee.volumetric_samples = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 8
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 4
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 8
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 1
|
||||
bpy.context.scene.cycles.volume_step_rate = 1
|
||||
bpy.context.scene.cycles.volume_bounces = 8
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_biased = True
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
||||
bpy.context.scene.cycles.volume_step_rate = 10
|
||||
|
||||
elif self.preset == "quality":
|
||||
# EEVEE
|
||||
# bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||
# bpy.context.scene.eevee.use_shadow_jitter_viewport = True
|
||||
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
|
||||
|
||||
# Cycles
|
||||
bpy.context.scene.cycles.volume_bounces = 8
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||
bpy.context.scene.cycles.volume_preview_step_rate = 0.5
|
||||
bpy.context.scene.cycles.volume_step_rate = 0.5
|
||||
bpy.context.scene.cycles.volume_bounces = 16
|
||||
bpy.context.scene.cycles.transparent_max_bounces = 32
|
||||
bpy.context.scene.cycles.volume_biased = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SliceViewer(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.slice_viewer"
|
||||
bl_label = "Slice Viewer"
|
||||
bl_description = "A preview scene setting for viewing slicers"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def execute(self, context):
|
||||
# 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 = 128
|
||||
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||
|
||||
bpy.context.scene.eevee.use_shadows = False
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = False
|
||||
|
||||
view_3d = None
|
||||
if context.area.type == 'VIEW_3D':
|
||||
view_3d = context.area
|
||||
else:
|
||||
for area in context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
view_3d = area
|
||||
break
|
||||
if view_3d:
|
||||
view_3d.spaces[0].shading.type = 'MATERIAL'
|
||||
view_3d.spaces[0].shading.studio_light = 'studio.exr'
|
||||
view_3d.spaces[0].shading.studiolight_intensity = 1.5
|
||||
view_3d.spaces[0].shading.use_scene_lights = False
|
||||
view_3d.spaces[0].shading.use_scene_world = False
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_all_layers(layer_filter=None):
|
||||
def _layer_filter(layer_obj):
|
||||
return True
|
||||
|
||||
layer_filter = layer_filter or _layer_filter
|
||||
layer_objs = get_all_layer_objs()
|
||||
return [obj for obj in layer_objs if layer_filter(obj)]
|
||||
|
||||
|
||||
class SaveAllLayersCache(bpy.types.Operator, SaveLayersCache):
|
||||
bl_idname = "bioxelnodes.save_all_layers_cache"
|
||||
bl_label = "Save All Layers Cache"
|
||||
bl_description = "Save all cache of this file"
|
||||
bl_icon = "FILE_TICK"
|
||||
|
||||
success_msg = "Successfully saved all layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_not_missing(layer_obj):
|
||||
return not is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_not_missing)
|
||||
|
||||
|
||||
class RemoveAllMissingLayers(bpy.types.Operator, RemoveLayers):
|
||||
bl_idname = "bioxelnodes.remove_all_missing_layers"
|
||||
bl_label = "Remove All Missing Layers"
|
||||
bl_description = "Remove all current container missing layers"
|
||||
bl_icon = "BRUSH_DATA"
|
||||
|
||||
success_msg = "Successfully removed all missing layers."
|
||||
|
||||
def get_layers(self, context):
|
||||
def is_missing(layer_obj):
|
||||
return is_missing_layer(layer_obj)
|
||||
return get_all_layers(is_missing)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.invoke_confirm(self,
|
||||
event,
|
||||
message=f"Are you sure to remove all **Missing** layers?")
|
||||
return {'RUNNING_MODAL'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class Help(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.help"
|
||||
bl_idname = "bioxel.help"
|
||||
bl_label = "Need Help?"
|
||||
bl_description = "Online Manual for Beginner"
|
||||
bl_icon = "HELP"
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open(
|
||||
'https://omoolab.github.io/BioxelNodes/latest/', new=2)
|
||||
webbrowser.open("https://omoolab.github.io/BioxelNodes/latest/", new=2)
|
||||
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AddEeveeEnv(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_eevee_env"
|
||||
bl_label = "Add EEVEE Env Light"
|
||||
bl_description = "To make volume shadow less dark"
|
||||
# 定义虚无化切换操作
|
||||
class TogglePhantom(bpy.types.Operator):
|
||||
"""Toggle object phantom state (ray invisible, no shadows, semi-transparent wireframe)"""
|
||||
|
||||
bl_idname = "bioxel.toggle_phantom"
|
||||
bl_label = "Toggle Phantom"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
bpy.ops.wm.append(
|
||||
directory=f"{str(LATEST_NODE_LIB_PATH)}/Object",
|
||||
filename="EeveeEnv"
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
self.report({"WARNING"}, "No object selected")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 获取3D视图空间
|
||||
view_space = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == "VIEW_3D":
|
||||
for space in area.spaces:
|
||||
if space.type == "VIEW_3D":
|
||||
view_space = space
|
||||
break
|
||||
if view_space:
|
||||
break
|
||||
|
||||
# 确保存储状态的文本数据块存在
|
||||
text_name = "phantom_orig_states"
|
||||
if text_name not in bpy.data.texts:
|
||||
bpy.data.texts.new(text_name)
|
||||
|
||||
# 从文本数据块加载状态
|
||||
states_text = bpy.data.texts[text_name]
|
||||
try:
|
||||
states = json.loads(states_text.as_string())
|
||||
except:
|
||||
states = {}
|
||||
|
||||
def get_obj_uuid(obj):
|
||||
if "bioxel_uuid" not in obj:
|
||||
obj["bioxel_uuid"] = uuid.uuid4().hex
|
||||
return obj["bioxel_uuid"]
|
||||
|
||||
# 使用物体的唯一标识符来追踪
|
||||
obj_uuid = get_obj_uuid(obj)
|
||||
|
||||
# 检查物体是否已经处于虚无化状态
|
||||
is_phantom = (
|
||||
obj.display_type == "SOLID"
|
||||
and obj.show_wire
|
||||
and obj.show_in_front
|
||||
and round(obj.color[3], 2) == 0.10
|
||||
and not obj.visible_camera
|
||||
and not obj.visible_diffuse
|
||||
and not obj.visible_glossy
|
||||
and not obj.visible_transmission
|
||||
and not obj.visible_volume_scatter
|
||||
and not obj.visible_shadow
|
||||
)
|
||||
|
||||
bpy.context.scene.eevee.use_shadows = True
|
||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||
if is_phantom:
|
||||
# 恢复原始状态
|
||||
if obj_uuid in states:
|
||||
state = states[obj_uuid]
|
||||
obj.display_type = state["display_type"]
|
||||
obj.show_wire = state["show_wire"]
|
||||
obj.show_in_front = state["show_in_front"]
|
||||
obj.color = state["color"]
|
||||
obj.visible_camera = state["visible_camera"]
|
||||
obj.visible_diffuse = state["visible_diffuse"]
|
||||
obj.visible_glossy = state["visible_glossy"]
|
||||
obj.visible_transmission = state["visible_transmission"]
|
||||
obj.visible_volume_scatter = state["visible_volume_scatter"]
|
||||
obj.visible_shadow = state["visible_shadow"]
|
||||
|
||||
return {'FINISHED'}
|
||||
# 恢复视图着色设置(如果之前有保存)
|
||||
if view_space and "viewport_color_type" in state:
|
||||
view_space.shading.color_type = state["viewport_color_type"]
|
||||
|
||||
del states[obj_uuid]
|
||||
self.report({"INFO"}, f"Object restored")
|
||||
else:
|
||||
# 保存原始状态
|
||||
state_data = {
|
||||
"display_type": obj.display_type,
|
||||
"show_wire": obj.show_wire,
|
||||
"show_in_front": obj.show_in_front,
|
||||
"color": list(obj.color), # 保存RGBA颜色值
|
||||
"visible_camera": obj.visible_camera,
|
||||
"visible_diffuse": obj.visible_diffuse,
|
||||
"visible_glossy": obj.visible_glossy,
|
||||
"visible_transmission": obj.visible_transmission,
|
||||
"visible_volume_scatter": obj.visible_volume_scatter,
|
||||
"visible_shadow": obj.visible_shadow,
|
||||
}
|
||||
|
||||
# 检查并处理视图着色设置
|
||||
if view_space:
|
||||
# 保存当前视图着色设置
|
||||
state_data["viewport_color_type"] = view_space.shading.color_type
|
||||
|
||||
# 检查当前是否为object, random或attribute
|
||||
valid_types = {"OBJECT", "RANDOM", "ATTRIBUTE"}
|
||||
if view_space.shading.color_type not in valid_types:
|
||||
# 改为object模式
|
||||
view_space.shading.color_type = "OBJECT"
|
||||
|
||||
states[obj_uuid] = state_data
|
||||
|
||||
# 设置虚无化状态
|
||||
obj.display_type = "SOLID" # 以实体方式显示
|
||||
obj.show_wire = True # 显示线框
|
||||
obj.show_in_front = True # 显示在其他物体前面
|
||||
# 保持RGB不变,仅修改alpha透明度为0.1
|
||||
obj.color[3] = 0.1
|
||||
# 关闭所有射线可见性和投影
|
||||
obj.visible_camera = False
|
||||
obj.visible_diffuse = False
|
||||
obj.visible_glossy = False
|
||||
obj.visible_transmission = False
|
||||
obj.visible_volume_scatter = False
|
||||
obj.visible_shadow = False
|
||||
self.report({"INFO"}, f"Object phantomed")
|
||||
|
||||
# 保存状态到文本数据块
|
||||
states_text.clear()
|
||||
states_text.write(json.dumps(states, indent=2))
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import bpy
|
||||
|
||||
from ..bioxelutils.common import is_incompatible, local_lib_not_updated
|
||||
from ..bioxelutils.node import assign_node_tree, get_node_tree
|
||||
from ..utils import is_incompatible, local_lib_not_updated
|
||||
from ..node import assign_node_tree, get_node_tree
|
||||
from ..utils import get_use_link
|
||||
|
||||
|
||||
class AddNode(bpy.types.Operator):
|
||||
bl_idname = "bioxelnodes.add_node"
|
||||
bl_idname = "bioxel.add_node"
|
||||
bl_label = "Add Node"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@@ -24,7 +24,7 @@ class AddNode(bpy.types.Operator):
|
||||
|
||||
@property
|
||||
def node_type(self):
|
||||
return f"BioxelNodes_{self.node_name}"
|
||||
return f"O {self.node_name}"
|
||||
|
||||
def execute(self, context):
|
||||
space = context.space_data
|
||||
|
||||
107
src/bioxelnodes/operators/structure.py
Normal file
107
src/bioxelnodes/operators/structure.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import bpy
|
||||
from ..utils import get_use_link
|
||||
from ..node import add_node_to_graph, get_output_node, get_main_node_group
|
||||
|
||||
|
||||
class ExtractMesh(bpy.types.Operator):
|
||||
bl_idname = "bioxel.extract_mesh"
|
||||
bl_label = "Extract Mesh"
|
||||
bl_description = "Extract Mesh"
|
||||
bl_icon = "OUTLINER_OB_MESH"
|
||||
bl_options = {"UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Check if there is a selected node in the node group
|
||||
node_group = get_main_node_group(context)
|
||||
selected_nodes = [n for n in node_group.nodes if n.select]
|
||||
|
||||
if len(selected_nodes) == 0:
|
||||
return False
|
||||
else:
|
||||
source_node = selected_nodes[0]
|
||||
return "Structure" in source_node.outputs
|
||||
|
||||
def execute(self, context):
|
||||
container_obj = context.object
|
||||
|
||||
if container_obj is None:
|
||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
||||
return {"FINISHED"}
|
||||
|
||||
node_group = container_obj.modifiers[0].node_group
|
||||
selected_nodes = [n for n in node_group.nodes if n.select]
|
||||
|
||||
source_node = selected_nodes[0]
|
||||
|
||||
# Check if the selected node has a "Structure" output
|
||||
if "Structure" not in source_node.outputs:
|
||||
self.report({"ERROR"}, "Selected node has no 'Structure' output.")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# 记录原始输出连接(用于后续复原)
|
||||
output_node = get_output_node(node_group)
|
||||
original_links = list(output_node.inputs[0].links)
|
||||
|
||||
# 添加Component to Mesh节点
|
||||
to_mesh_node = add_node_to_graph(
|
||||
"Structure to Mesh", node_group, use_link=get_use_link()
|
||||
)
|
||||
|
||||
# 连接节点:源节点 -> Component to Mesh -> 输出节点
|
||||
node_group.links.new(
|
||||
source_node.outputs["Structure"], to_mesh_node.inputs[0])
|
||||
node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0])
|
||||
|
||||
new_obj = None
|
||||
|
||||
try:
|
||||
deps = context.evaluated_depsgraph_get()
|
||||
eval_obj = container_obj.evaluated_get(deps)
|
||||
# obtain evaluated mesh (from modifiers including Geometry Nodes)
|
||||
mesh_eval = eval_obj.to_mesh()
|
||||
if mesh_eval is None:
|
||||
raise RuntimeError("Failed to produce evaluated mesh")
|
||||
|
||||
mesh_name = f"{container_obj.name}_Mesh"
|
||||
mesh_copy = mesh_eval.copy()
|
||||
mesh_copy.name = mesh_name
|
||||
# clear the temporary evaluated mesh
|
||||
try:
|
||||
eval_obj.to_mesh_clear()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# create the new object with same transform as source
|
||||
new_obj = bpy.data.objects.new(mesh_name, mesh_copy)
|
||||
# link to same collection as the container (fallback to current collection)
|
||||
col = (
|
||||
container_obj.users_collection[0]
|
||||
if getattr(container_obj, "users_collection", None)
|
||||
else context.collection
|
||||
)
|
||||
if col is None:
|
||||
col = context.collection
|
||||
col.objects.link(new_obj)
|
||||
|
||||
# copy world transform exactly
|
||||
new_obj.matrix_world = container_obj.matrix_world.copy()
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to extract mesh: {e}")
|
||||
|
||||
# 恢复原始连接
|
||||
for link in original_links:
|
||||
node_group.links.new(link.from_socket, output_node.inputs[0])
|
||||
|
||||
# 删除临时添加的Component to Mesh节点
|
||||
node_group.nodes.remove(to_mesh_node)
|
||||
if new_obj:
|
||||
# deselect everything, select and activate new_obj
|
||||
for o in list(context.selected_objects):
|
||||
o.select_set(False)
|
||||
new_obj.select_set(True)
|
||||
context.view_layer.objects.active = new_obj
|
||||
self.report({"INFO"}, f"Mesh extracted: {new_obj.name}")
|
||||
return {"FINISHED"}
|
||||
else:
|
||||
return {"CANCELLED"}
|
||||
Reference in New Issue
Block a user