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:
2026-01-21 12:42:12 +08:00
parent afae532a2c
commit a558d1ae30
43 changed files with 2933 additions and 3701 deletions

180
src/bioxelnodes/panels.py Normal file
View File

@@ -0,0 +1,180 @@
from pathlib import Path
import bpy
from .node import get_layer_nodes, get_main_node_group
from .utils import load_icon
from .layer import get_layer_caches
from .operators.io import ImportAsColor, ImportAsLabel, ImportAsScalar, ImportData
from .operators.misc import Help, RenderSettingPreset
from .operators.layer import (
AddLayerNode,
DeleteLayer,
RelocatePath,
RenameLayer,
SaveLayer,
)
class ImportMenu(bpy.types.Menu):
bl_label = "Bioxel Import"
bl_idname = "BIOXEL_MT_import"
def draw(self, context):
layout = self.layout
layout.operator(ImportAsScalar.bl_idname,
text="Import As Scalar").filepath = ""
layout.operator(ImportAsLabel.bl_idname,
text="Import As Label").filepath = ""
layout.operator(ImportAsColor.bl_idname,
text="Import As Color").filepath = ""
class RenderSettingMenu(bpy.types.Menu):
bl_label = "Render Setting Presets"
bl_idname = "BIOXEL_MT_render_setting"
def draw(self, context):
layout = self.layout
for k, v in RenderSettingPreset.PRESETS.items():
op = layout.operator(RenderSettingPreset.bl_idname, text=v)
op.preset = k
class BioxelPanelBase:
"""
Abstract base panel for all Bioxel UI panels.
- Ensures the panel appears only when the active area is the Node Editor
and the Node Editor is editing a Geometry Nodes tree.
- Places the panel under the "Bioxel" sidebar tab.
- Provides an optional bl_order for panel ordering inside the category.
"""
bl_space_type = "NODE_EDITOR"
bl_region_type = "UI"
bl_context = "geometry_node"
bl_category = "胞素(Bioxel)"
# try to make Bioxel panels order early (may be respected by Blender)
@classmethod
def poll(cls, context):
node_group = get_main_node_group(context)
return node_group is not None
class HeaderPanel(bpy.types.Panel, BioxelPanelBase):
bl_label = "胞素(Bioxel)"
bl_idname = "BIOXEL_PT_header_panel"
bl_order = 0
def draw(self, context):
layout = self.layout
layout.operator(
Help.bl_idname, text="Help", icon=Help.bl_icon # 复用原操作的图标定义
)
layout.menu(
RenderSettingMenu.bl_idname, text="Render Preset", icon="RENDER_STILL"
)
class LayerNodePanel(BioxelPanelBase, bpy.types.Panel):
bl_label = "Layer Nodes"
bl_idname = "BIOXEL_PT_o_layer_nodes"
bl_order = 1
def draw(self, context):
layout = self.layout
node_group = get_main_node_group(context)
layout.template_list(
"BIOXEL_UL_layer_nodes",
"",
node_group,
"nodes",
context.window_manager,
"bioxel_layer_list_index",
rows=3,
)
class LibraryPanel(BioxelPanelBase, bpy.types.Panel):
bl_label = "Layer Library"
bl_idname = "BIOXEL_PT_library_panel"
bl_order = 2
def draw(self, context):
layout = self.layout
wm = context.window_manager
caches = get_layer_caches()
row = layout.row()
row.scale_y = 2.0
row.operator(ImportData.bl_idname, icon="IMPORT")
# row.menu(ImportMenu.bl_idname, icon="IMPORT", text="Import")
layout.separator()
layout.template_icon_view(
bpy.context.window_manager,
"bioxel_layer_library",
show_labels=False,
scale=10.0,
scale_popup=6.0,
)
selected_id = getattr(wm, "bioxel_layer_library", None)
selected_id = None if selected_id == "nothing_found" else selected_id
entry = next((c for c in caches if c.get("id") == selected_id), None)
if entry:
cache_id = entry["id"]
path = entry["path"]
name = entry.get("name", "?")
kind = entry.get("kind", "?")
path_exists = Path(bpy.path.abspath(path)).exists()
layout.prop(wm, "bioxel_snapshot_z", text="Z Slice", slider=True)
layout.separator()
# operations and meta_box as before
ops_row = layout.row(align=True)
if path_exists:
ops_row.operator(
AddLayerNode.bl_idname, text="Add", icon="PLUS"
).cache_id = selected_id
ops_row.operator(
SaveLayer.bl_idname, text="Save", icon="FILE_TICK"
).cache_id = selected_id
ops_row.operator(
RenameLayer.bl_idname, text="Name", icon="FONT_DATA"
).cache_id = selected_id
ops_row.operator(
DeleteLayer.bl_idname, text="", icon="TRASH"
).cache_id = selected_id
else:
ops_row.operator(
RelocatePath.bl_idname, text="Relocate", icon="FILE_FOLDER"
).cache_id = selected_id
ops_row.operator(
DeleteLayer.bl_idname, text="", icon="TRASH"
).cache_id = selected_id
meta_box = layout.box()
if hasattr(meta_box, "alert"):
meta_box.alert = True
meta_box.label(text=f"Cache is missing...")
meta_box.label(text=f"Please relocate the cache path!")
return
meta_box = layout.box()
meta_box.label(text=f"Name: {name}")
meta_box.label(text=f"Path: {path}")
meta_box.label(text=f"Kind: {kind}")
meta_box.label(
text=f"Dims: [{entry.get('frame_count',1)},{tuple(entry.get('shape',''))},{entry.get('channel_count',1)}]"
)
if kind in ["scalar", "color", "vector"]:
path = Path(bpy.path.abspath(entry["path"]))
histogram = load_icon(
path / "histogram.png", f"{cache_id}_histogram")
meta_box.template_icon(histogram, scale=10.0)
else:
layout.label(text="No layer selected", icon="QUESTION")