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:
12
.gitignore
vendored
12
.gitignore
vendored
@@ -125,14 +125,14 @@ dmypy.json
|
||||
|
||||
pythonlib*
|
||||
|
||||
*.blend1
|
||||
|
||||
blendcache_*
|
||||
|
||||
*.ipynb
|
||||
|
||||
.secrets
|
||||
|
||||
.vdb
|
||||
|
||||
!scipy_ndimage/*/**
|
||||
!scipy_ndimage/*/**
|
||||
|
||||
# Blender
|
||||
*.blend1
|
||||
blendcache_*
|
||||
*.cats.txt~
|
||||
222
AGENTS.md
Normal file
222
AGENTS.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# BioxelNodes - Agent Development Guide
|
||||
|
||||
This file contains essential information for agentic coding agents working on the BioxelNodes codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
BioxelNodes is a Blender addon for scientific volumetric data visualization. It integrates with Blender's Geometry Nodes and Cycles rendering engine to process and visualize medical/scientific data.
|
||||
|
||||
**Language:** Python 3.11
|
||||
**Framework:** Blender Python API (bpy)
|
||||
**Package Manager:** uv
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Environment Setup
|
||||
```bash
|
||||
# Install dependencies
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
```bash
|
||||
# Build for specific platform
|
||||
uv run build.py <platform> # platforms: windows-x64, linux-x64, macos-arm64, macos-x64
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
```bash
|
||||
# Format code (autopep8)
|
||||
autopep8 --in-place --recursive src/
|
||||
```
|
||||
|
||||
### Documentation
|
||||
```bash
|
||||
# Serve documentation locally
|
||||
uv run mkdocs serve
|
||||
|
||||
# Build documentation
|
||||
uv run mike deploy --push --update-aliases 0.2.x latest
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Formatting
|
||||
- **4-space indentation**
|
||||
- **PEP 8** compliance enforced via autopep8
|
||||
|
||||
### Naming Conventions
|
||||
- **snake_case** for functions, variables, and files
|
||||
- **PascalCase** for classes (Blender convention)
|
||||
- **UPPER_CASE** for constants
|
||||
- **bioxel_** prefix for addon-specific properties
|
||||
|
||||
### Import Organization
|
||||
```python
|
||||
# Standard library imports first
|
||||
import pathlib
|
||||
import uuid
|
||||
|
||||
# Third-party imports
|
||||
import bpy
|
||||
import numpy as np
|
||||
import SimpleITK as sitk
|
||||
|
||||
# Relative imports for internal modules
|
||||
from ..exceptions import CancelledByUser
|
||||
from ..utils import get_layer_obj
|
||||
```
|
||||
|
||||
### Type Hints
|
||||
- Use sparingly, mainly for Blender property annotations
|
||||
- Focus on function signatures and complex data structures
|
||||
- Example: `def process_data(data: np.ndarray) -> np.ndarray:`
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Auto-Registration System
|
||||
- Uses `auto_load.py` for automatic class registration
|
||||
- Classes are discovered automatically via reflection
|
||||
- Registration order resolved via topological sorting
|
||||
- All Blender classes must inherit from appropriate base types
|
||||
|
||||
### Module Structure
|
||||
```
|
||||
src/bioxelnodes/
|
||||
├── __init__.py # Addon entry point, asset library setup
|
||||
├── auto_load.py # Auto-registration system
|
||||
├── constants.py # Constants and paths
|
||||
├── utils.py # Utility functions
|
||||
├── preferences.py # Addon preferences
|
||||
├── props.py # Blender properties
|
||||
├── panels.py # UI panels
|
||||
├── menus.py # UI menus
|
||||
├── node.py # Node utilities
|
||||
├── layer.py # Layer management
|
||||
├── operators/ # Blender operators
|
||||
├── bioxel/ # Core bioxel functionality
|
||||
└── assets/ # Blender assets and node libraries
|
||||
```
|
||||
|
||||
### Blender Integration Patterns
|
||||
|
||||
#### Property Groups
|
||||
```python
|
||||
class BioxelProperties(bpy.types.PropertyGroup):
|
||||
bl_label = "Bioxel Properties"
|
||||
|
||||
bioxel_custom_prop: bpy.props.StringProperty(
|
||||
name="Custom Property",
|
||||
default="",
|
||||
description="Description for UI"
|
||||
)
|
||||
```
|
||||
|
||||
#### Operators
|
||||
```python
|
||||
class BIOXEL_OT_custom_operator(bpy.types.Operator):
|
||||
bl_idname = "bioxel.custom_operator"
|
||||
bl_label = "Custom Operator"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
# Operator logic here
|
||||
return {'FINISHED'}
|
||||
```
|
||||
|
||||
#### Panels
|
||||
```python
|
||||
class BIOXEL_PT_custom_panel(bpy.types.Panel):
|
||||
bl_label = "Custom Panel"
|
||||
bl_idname = "BIOXEL_PT_custom_panel"
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = "Bioxel"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
# UI drawing code here
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Handling Patterns
|
||||
- Use try-catch blocks for file operations
|
||||
- Handle user cancellation gracefully
|
||||
- Report progress for long operations using `bioxel_progress_factor` and `bioxel_progress_text`
|
||||
- Log warnings with print statements for debugging
|
||||
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
### Core Libraries
|
||||
- **bpy** (Blender Python API) - Core integration
|
||||
- **numpy** - Array processing
|
||||
- **SimpleITK** - Medical image processing
|
||||
- **h5py** - HDF5 file handling
|
||||
|
||||
### Scientific Libraries
|
||||
- **matplotlib** - Plotting and visualization
|
||||
- **transforms3d** - 3D transformations
|
||||
- **pyometiff** - OME-TIFF support
|
||||
- **mrcfile** - MRC file format support
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes
|
||||
1. Identify the appropriate module (operators, panels, bioxel, etc.)
|
||||
2. Follow existing naming conventions and patterns
|
||||
3. Use auto-registration - no manual registration needed
|
||||
4. Test with Blender's Python console
|
||||
5. Format code with autopep8 before committing
|
||||
|
||||
### Adding New Features
|
||||
1. Create appropriate classes in relevant modules
|
||||
2. Use proper prefixes (BIOXEL_OT_, BIOXEL_PT_, etc.)
|
||||
3. Add to auto_load system automatically discovers new classes
|
||||
4. Update documentation if needed
|
||||
|
||||
### File Operations
|
||||
- Use `pathlib.Path` for path operations
|
||||
- Check file existence before operations
|
||||
- Handle permissions and missing files gracefully
|
||||
- Use appropriate file formats (HDF5 for data, PNG for images)
|
||||
|
||||
## Blender Integration Notes
|
||||
|
||||
### Asset Library
|
||||
- Automatically managed via `add_asset_library_if_missing()`
|
||||
- Located at `NODE_LIB_DIRPATH`
|
||||
- Uses "PACK" import method
|
||||
- Name: "O Bioxel"
|
||||
|
||||
### Progress Reporting
|
||||
- Use `bioxel_progress_factor` (0.0 to 1.0) for progress
|
||||
- Use `bioxel_progress_text` for status messages
|
||||
- Update via WindowManager properties
|
||||
|
||||
### Layer Management
|
||||
- Layers stored as HDF5 files
|
||||
- Container objects manage multiple layers
|
||||
- Use `layer.py` for layer operations
|
||||
- Support for various medical image formats
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Blender API
|
||||
- Always check context validity
|
||||
- Handle different Blender versions (check `bpy.app.version`)
|
||||
- Use proper property types for UI
|
||||
- Register classes in correct order (auto_load handles this)
|
||||
|
||||
### Performance
|
||||
- Use NumPy for array operations
|
||||
- Avoid expensive operations in UI draw calls
|
||||
- Cache computed values where appropriate
|
||||
- Use background threads for long operations
|
||||
|
||||
### File I/O
|
||||
- Always close file handles
|
||||
- Use context managers for file operations
|
||||
- Handle large files in chunks
|
||||
- Validate file formats before processing
|
||||
4
build.py
4
build.py
@@ -17,7 +17,9 @@ required_packages = ["SimpleITK==2.3.1",
|
||||
"mrcfile==1.5.1",
|
||||
"h5py==3.11.0",
|
||||
"transforms3d==0.4.2",
|
||||
"tifffile==2024.7.24"]
|
||||
"tifffile==2024.7.24",
|
||||
"matplotlib==3.10.7",
|
||||
"pillow==11.2.1"]
|
||||
|
||||
|
||||
platforms = {"windows-x64": Platform(pypi_suffix="win_amd64",
|
||||
|
||||
92
pack.py
Normal file
92
pack.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Create a zip archive of the src/bioxelnodes package with a top-level folder
|
||||
named `bioxelnodes/`. Excludes specified temporary/cache files and folders.
|
||||
Does not modify any files in the project.
|
||||
|
||||
Usage:
|
||||
python build_package.py <platform_name> [output_zip_path]
|
||||
|
||||
Example:
|
||||
python build_package.py windows-x64
|
||||
"""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import zipfile
|
||||
import fnmatch
|
||||
from datetime import datetime
|
||||
|
||||
platforms = {
|
||||
"windows-x64": "windows-x64",
|
||||
"linux-x64": "linux-x64",
|
||||
"macos-arm64": "macos-arm64",
|
||||
"macos-x64": "macos-x64",
|
||||
}
|
||||
|
||||
# patterns to exclude (filename matching)
|
||||
EXCLUDE_FILENAME_PATTERNS = [
|
||||
"*.blend1",
|
||||
"blendcache_*",
|
||||
"*.cats.txt~",
|
||||
"*.pyc",
|
||||
"*.pyo",
|
||||
"*$py.class",
|
||||
]
|
||||
|
||||
# directory names to exclude entirely
|
||||
EXCLUDE_DIR_NAMES = {
|
||||
"__pycache__",
|
||||
}
|
||||
|
||||
def should_exclude(path: Path) -> bool:
|
||||
name = path.name
|
||||
# exclude directories by name anywhere in the path
|
||||
if any(part in EXCLUDE_DIR_NAMES for part in path.parts):
|
||||
return True
|
||||
# only apply filename patterns to files
|
||||
if path.is_file():
|
||||
for pat in EXCLUDE_FILENAME_PATTERNS:
|
||||
if fnmatch.fnmatch(name, pat):
|
||||
return True
|
||||
return False
|
||||
|
||||
def build_package(platform_name: str, out_zip: Path):
|
||||
src_root = Path("src") / "bioxelnodes"
|
||||
if not src_root.exists():
|
||||
raise SystemExit(f"Source folder not found: {src_root.resolve()}")
|
||||
|
||||
out_zip.parent.mkdir(parents=True, exist_ok=True)
|
||||
total = 0
|
||||
with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||
for p in src_root.rglob("*"):
|
||||
if p.is_dir():
|
||||
continue
|
||||
if should_exclude(p):
|
||||
continue
|
||||
# arcname should place files under top-level 'bioxelnodes/...'
|
||||
rel = p.relative_to(src_root)
|
||||
arcname = Path("bioxelnodes") / rel
|
||||
zf.write(p, arcname.as_posix())
|
||||
total += 1
|
||||
print(f"Packaged {total} files into {out_zip}")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python build_package.py <platform_name> [output_zip_path]")
|
||||
print("Available platforms:", ", ".join(platforms.keys()))
|
||||
sys.exit(1)
|
||||
|
||||
platform_key = sys.argv[1]
|
||||
if platform_key not in platforms:
|
||||
print("Unknown platform:", platform_key)
|
||||
print("Available platforms:", ", ".join(platforms.keys()))
|
||||
sys.exit(1)
|
||||
|
||||
tag = platforms[platform_key]
|
||||
timestamp = datetime.now().strftime("%Y%m%dT%H%M%S")
|
||||
default_name = f"bioxelnodes-{tag}-{timestamp}.zip"
|
||||
out_path = Path(sys.argv[2]) if len(sys.argv) >= 3 else Path(default_name)
|
||||
|
||||
build_package(platform_key, out_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
"mrcfile==1.5.1",
|
||||
"h5py==3.11.0",
|
||||
"transforms3d==0.4.2",
|
||||
"matplotlib==3.10.7",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
@@ -25,6 +26,7 @@ dev = [
|
||||
"autopep8>=2.3.1,<3",
|
||||
"mike>=2.1.2,<3",
|
||||
"tomlkit>=0.13.0,<0.14",
|
||||
"pip>=25.3",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -1,26 +1,109 @@
|
||||
import bpy
|
||||
import bpy.utils.previews as previews
|
||||
|
||||
from .constants import NODE_LIB_DIRPATH, PREVIEW_COLLECTIONS
|
||||
from .props import _bioxel_layer_items, _update_layer_gallery, _update_snapshot_z
|
||||
|
||||
from .props import BIOXELNODES_LayerListUL
|
||||
from . import auto_load
|
||||
from . import menus
|
||||
|
||||
|
||||
auto_load.init()
|
||||
|
||||
|
||||
def add_asset_library_if_missing():
|
||||
"""检查并添加Asset Library(如果不存在),使用link导入方式"""
|
||||
# 检查路径是否存在
|
||||
if not NODE_LIB_DIRPATH.exists():
|
||||
print(f"警告: 节点库路径不存在 - {NODE_LIB_DIRPATH}")
|
||||
return
|
||||
|
||||
# 转换为字符串路径用于Blender设置
|
||||
lib_path_str = str(NODE_LIB_DIRPATH)
|
||||
|
||||
# 检查是否已存在该资产库
|
||||
prefs = bpy.context.preferences.filepaths.asset_libraries
|
||||
for lib in prefs:
|
||||
if lib.path == lib_path_str:
|
||||
return # 已存在,无需添加
|
||||
|
||||
# 添加新的资产库,使用link导入方式
|
||||
new_lib = prefs.new()
|
||||
new_lib.name = "O Bioxel" # 资产库名称
|
||||
new_lib.path = lib_path_str # 资产库路径(字符串形式)
|
||||
new_lib.import_method = "PACK" # 设置导入方式为link
|
||||
|
||||
print(f"Add Bioxel Nodes library: {lib_path_str}")
|
||||
|
||||
|
||||
def remove_asset_library_if_exists():
|
||||
"""Remove the Bioxel asset library entry if it was added (by path or name)."""
|
||||
# if path missing, nothing to remove
|
||||
lib_path_str = str(NODE_LIB_DIRPATH)
|
||||
prefs = bpy.context.preferences.filepaths.asset_libraries
|
||||
|
||||
# iterate over a snapshot of prefs to safely find index
|
||||
lib_to_remove = None
|
||||
for i, lib in enumerate(list(prefs)):
|
||||
try:
|
||||
if lib.path == lib_path_str:
|
||||
lib_to_remove = lib
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if lib_to_remove is not None:
|
||||
try:
|
||||
prefs.remove(lib_to_remove)
|
||||
print(f"Removed Bioxel Nodes library: {lib_path_str}")
|
||||
except Exception as e:
|
||||
print(f"Warning: failed to remove Bioxel node library: {e}")
|
||||
|
||||
|
||||
def register():
|
||||
pcoll = previews.new()
|
||||
pcoll.layer_previews = ()
|
||||
PREVIEW_COLLECTIONS["layers"] = pcoll
|
||||
|
||||
bpy.types.WindowManager.bioxel_progress_factor = bpy.props.FloatProperty(
|
||||
default=1.0
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.bioxel_progress_text = bpy.props.StringProperty()
|
||||
|
||||
bpy.types.WindowManager.bioxel_layer_library = bpy.props.EnumProperty(
|
||||
name="Bioxel Layer Gallery",
|
||||
items=_bioxel_layer_items,
|
||||
update=_update_layer_gallery,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.bioxel_snapshot_z = bpy.props.FloatProperty(
|
||||
name="Snapshot Z",
|
||||
default=0.5,
|
||||
min=0.0,
|
||||
max=1.0,
|
||||
subtype="FACTOR",
|
||||
update=_update_snapshot_z,
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.bioxel_layer_list_index = bpy.props.IntProperty(
|
||||
default=0)
|
||||
|
||||
auto_load.register()
|
||||
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty(
|
||||
default=1.0)
|
||||
bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty()
|
||||
bpy.types.WindowManager.bioxelnodes_layer_list_UL = bpy.props.PointerProperty(
|
||||
type=BIOXELNODES_LayerListUL)
|
||||
menus.add()
|
||||
add_asset_library_if_missing()
|
||||
|
||||
|
||||
def unregister():
|
||||
remove_asset_library_if_exists()
|
||||
menus.remove()
|
||||
del bpy.types.WindowManager.bioxelnodes_progress_factor
|
||||
del bpy.types.WindowManager.bioxelnodes_progress_text
|
||||
del bpy.types.WindowManager.bioxelnodes_layer_list_UL
|
||||
auto_load.unregister()
|
||||
|
||||
del bpy.types.WindowManager.bioxel_progress_factor
|
||||
del bpy.types.WindowManager.bioxel_progress_text
|
||||
del bpy.types.WindowManager.bioxel_layer_library
|
||||
del bpy.types.WindowManager.bioxel_snapshot_z
|
||||
del bpy.types.WindowManager.bioxel_layer_list_index
|
||||
|
||||
for pcoll in PREVIEW_COLLECTIONS.values():
|
||||
previews.remove(pcoll)
|
||||
PREVIEW_COLLECTIONS.clear()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/bioxelnodes/assets/O Bioxel/BioxelNodes.blend
LFS
Normal file
BIN
src/bioxelnodes/assets/O Bioxel/BioxelNodes.blend
LFS
Normal file
Binary file not shown.
12
src/bioxelnodes/assets/O Bioxel/blender_assets.cats.txt
Normal file
12
src/bioxelnodes/assets/O Bioxel/blender_assets.cats.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
# This is an Asset Catalog Definition file for Blender.
|
||||
#
|
||||
# Empty lines and lines starting with `#` will be ignored.
|
||||
# The first non-ignored line should be the version indicator.
|
||||
# Other lines are of the format "UUID:catalog/path/for/assets:simple catalog name"
|
||||
|
||||
VERSION 1
|
||||
|
||||
c0699a8b-6fd6-4d10-bdde-5ea0fa36724d:O Bioxel:O Bioxel
|
||||
5ea24e10-5a2b-44d1-b67b-19014c412793:O Bioxel/Layer:O Bioxel-Layer
|
||||
17383d65-f1a0-4122-b73a-0baaad4b8ecf:O Bioxel/Structure:O Bioxel-Structure
|
||||
d03fc5b0-465c-45ba-ba8e-524a1c52ae3d:O Bioxel/Utilities:O Bioxel-Utilities
|
||||
@@ -80,11 +80,13 @@ def get_ordered_classes_to_register(modules):
|
||||
|
||||
def get_register_deps_dict(modules):
|
||||
my_classes = set(iter_my_classes(modules))
|
||||
my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
|
||||
my_classes_by_idname = {
|
||||
cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")}
|
||||
|
||||
deps_dict = {}
|
||||
for cls in my_classes:
|
||||
deps_dict[cls] = set(iter_my_register_deps(cls, my_classes, my_classes_by_idname))
|
||||
deps_dict[cls] = set(iter_my_register_deps(
|
||||
cls, my_classes, my_classes_by_idname))
|
||||
return deps_dict
|
||||
|
||||
|
||||
@@ -181,7 +183,8 @@ def toposort(deps_dict):
|
||||
sorted_values.add(value)
|
||||
else:
|
||||
unsorted.append(value)
|
||||
deps_dict = {value: deps_dict[value] - sorted_values for value in unsorted}
|
||||
deps_dict = {value: deps_dict[value] -
|
||||
sorted_values for value in unsorted}
|
||||
sorted_list_sub.sort(key=lambda cls: getattr(cls, "bl_order", 0))
|
||||
sorted_list.extend(sorted_list_sub)
|
||||
return sorted_list
|
||||
|
||||
@@ -78,8 +78,8 @@ class Layer():
|
||||
mask_frame = mask[f, :, :, :]
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.median_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
@@ -88,8 +88,8 @@ class Layer():
|
||||
mask_frame = mask[:, :, :]
|
||||
if smooth > 0:
|
||||
mask_frame = scipy.median_filter(mask_frame.astype(np.float32),
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
mode="nearest",
|
||||
size=smooth)
|
||||
# mask_frame = scipy.median_filter(
|
||||
# mask_frame.astype(np.float32), size=2)
|
||||
mask_frames += (mask_frame,)
|
||||
@@ -150,3 +150,32 @@ class Layer():
|
||||
|
||||
mat_scale = transforms3d.zooms.zfdir2aff(factors[0])
|
||||
self.affine = np.dot(self.affine, mat_scale)
|
||||
|
||||
def snapshot(self, shape: tuple, smooth: int = 0):
|
||||
if len(shape) != 3:
|
||||
raise Exception("Shape must be 3 dim")
|
||||
|
||||
data = self.data
|
||||
|
||||
snapshot = data[0, :, :, :, :]
|
||||
|
||||
factors = np.divide(self.shape, shape)
|
||||
zoom_factors = [1 / f for f in factors]
|
||||
order = 0 if snapshot.dtype == bool else 1
|
||||
snapshot = ndi.zoom(snapshot,
|
||||
zoom_factors+[1.0],
|
||||
mode="nearest",
|
||||
grid_mode=False,
|
||||
order=order)
|
||||
|
||||
snapshot = snapshot.astype(np.float32)
|
||||
|
||||
if self.kind in ['scalar', 'vector', 'color']:
|
||||
mn = float(np.nanmin(snapshot))
|
||||
mx = float(np.nanmax(snapshot))
|
||||
if mx - mn > 1e-8:
|
||||
snapshot = (snapshot - mn) / (mx - mn)
|
||||
else:
|
||||
snapshot = np.clip(snapshot, 0.0, 1.0)
|
||||
|
||||
return snapshot
|
||||
|
||||
@@ -39,7 +39,6 @@ from . import _ni_support
|
||||
from . import _nd_image
|
||||
|
||||
|
||||
|
||||
def _invalid_origin(origin, lenw):
|
||||
return (origin < -(lenw // 2)) or (origin > (lenw - 1) // 2)
|
||||
|
||||
@@ -73,7 +72,6 @@ def _complex_via_real_components(func, input, weights, output, cval, **kwargs):
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def correlate1d(input, weights, axis=-1, output=None, mode="reflect",
|
||||
cval=0.0, origin=0):
|
||||
"""Calculate a 1-D correlation along the given axis.
|
||||
@@ -133,7 +131,6 @@ def correlate1d(input, weights, axis=-1, output=None, mode="reflect",
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def convolve1d(input, weights, axis=-1, output=None, mode="reflect",
|
||||
cval=0.0, origin=0):
|
||||
"""Calculate a 1-D convolution along the given axis.
|
||||
@@ -205,7 +202,6 @@ def _gaussian_kernel1d(sigma, order, radius):
|
||||
return q * phi_x
|
||||
|
||||
|
||||
|
||||
def gaussian_filter1d(input, sigma, axis=-1, order=0, output=None,
|
||||
mode="reflect", cval=0.0, truncate=4.0, *, radius=None):
|
||||
"""1-D Gaussian filter.
|
||||
@@ -274,7 +270,6 @@ def gaussian_filter1d(input, sigma, axis=-1, order=0, output=None,
|
||||
return correlate1d(input, weights, axis, output, mode, cval, 0)
|
||||
|
||||
|
||||
|
||||
def gaussian_filter(input, sigma, order=0, output=None,
|
||||
mode="reflect", cval=0.0, truncate=4.0, *, radius=None,
|
||||
axes=None):
|
||||
@@ -381,7 +376,6 @@ def gaussian_filter(input, sigma, order=0, output=None,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def prewitt(input, axis=-1, output=None, mode="reflect", cval=0.0):
|
||||
"""Calculate a Prewitt filter.
|
||||
|
||||
@@ -443,7 +437,6 @@ def prewitt(input, axis=-1, output=None, mode="reflect", cval=0.0):
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0):
|
||||
"""Calculate a Sobel filter.
|
||||
|
||||
@@ -501,7 +494,6 @@ def sobel(input, axis=-1, output=None, mode="reflect", cval=0.0):
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def generic_laplace(input, derivative2, output=None, mode="reflect",
|
||||
cval=0.0,
|
||||
extra_arguments=(),
|
||||
@@ -549,7 +541,6 @@ def generic_laplace(input, derivative2, output=None, mode="reflect",
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def laplace(input, output=None, mode="reflect", cval=0.0):
|
||||
"""N-D Laplace filter based on approximate second derivatives.
|
||||
|
||||
@@ -584,7 +575,6 @@ def laplace(input, output=None, mode="reflect", cval=0.0):
|
||||
return generic_laplace(input, derivative2, output, mode, cval)
|
||||
|
||||
|
||||
|
||||
def gaussian_laplace(input, sigma, output=None, mode="reflect",
|
||||
cval=0.0, **kwargs):
|
||||
"""Multidimensional Laplace filter using Gaussian second derivatives.
|
||||
@@ -637,7 +627,6 @@ def gaussian_laplace(input, sigma, output=None, mode="reflect",
|
||||
extra_keywords=kwargs)
|
||||
|
||||
|
||||
|
||||
def generic_gradient_magnitude(input, derivative, output=None,
|
||||
mode="reflect", cval=0.0,
|
||||
extra_arguments=(), extra_keywords=None):
|
||||
@@ -690,7 +679,6 @@ def generic_gradient_magnitude(input, derivative, output=None,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def gaussian_gradient_magnitude(input, sigma, output=None,
|
||||
mode="reflect", cval=0.0, **kwargs):
|
||||
"""Multidimensional gradient magnitude using Gaussian derivatives.
|
||||
@@ -792,7 +780,6 @@ def _correlate_or_convolve(input, weights, output, mode, cval, origin,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def correlate(input, weights, output=None, mode='reflect', cval=0.0,
|
||||
origin=0):
|
||||
"""
|
||||
@@ -856,7 +843,6 @@ def correlate(input, weights, output=None, mode='reflect', cval=0.0,
|
||||
origin, False)
|
||||
|
||||
|
||||
|
||||
def convolve(input, weights, output=None, mode='reflect', cval=0.0,
|
||||
origin=0):
|
||||
"""
|
||||
@@ -967,7 +953,6 @@ def convolve(input, weights, output=None, mode='reflect', cval=0.0,
|
||||
origin, True)
|
||||
|
||||
|
||||
|
||||
def uniform_filter1d(input, size, axis=-1, output=None,
|
||||
mode="reflect", cval=0.0, origin=0):
|
||||
"""Calculate a 1-D uniform filter along the given axis.
|
||||
@@ -1018,7 +1003,6 @@ def uniform_filter1d(input, size, axis=-1, output=None,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def uniform_filter(input, size=3, output=None, mode="reflect",
|
||||
cval=0.0, origin=0, *, axes=None):
|
||||
"""Multidimensional uniform filter.
|
||||
@@ -1088,7 +1072,6 @@ def uniform_filter(input, size=3, output=None, mode="reflect",
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def minimum_filter1d(input, size, axis=-1, output=None,
|
||||
mode="reflect", cval=0.0, origin=0):
|
||||
"""Calculate a 1-D minimum filter along the given axis.
|
||||
@@ -1145,7 +1128,6 @@ def minimum_filter1d(input, size, axis=-1, output=None,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def maximum_filter1d(input, size, axis=-1, output=None,
|
||||
mode="reflect", cval=0.0, origin=0):
|
||||
"""Calculate a 1-D maximum filter along the given axis.
|
||||
@@ -1302,7 +1284,6 @@ def _min_or_max_filter(input, size, footprint, structure, output, mode,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def minimum_filter(input, size=None, footprint=None, output=None,
|
||||
mode="reflect", cval=0.0, origin=0, *, axes=None):
|
||||
"""Calculate a multidimensional minimum filter.
|
||||
@@ -1350,7 +1331,6 @@ def minimum_filter(input, size=None, footprint=None, output=None,
|
||||
cval, origin, 1, axes)
|
||||
|
||||
|
||||
|
||||
def maximum_filter(input, size=None, footprint=None, output=None,
|
||||
mode="reflect", cval=0.0, origin=0, *, axes=None):
|
||||
"""Calculate a multidimensional maximum filter.
|
||||
@@ -1398,7 +1378,6 @@ def maximum_filter(input, size=None, footprint=None, output=None,
|
||||
cval, origin, 0, axes)
|
||||
|
||||
|
||||
|
||||
def _rank_filter(input, rank, size=None, footprint=None, output=None,
|
||||
mode="reflect", cval=0.0, origin=0, operation='rank',
|
||||
axes=None):
|
||||
@@ -1491,7 +1470,6 @@ def _rank_filter(input, rank, size=None, footprint=None, output=None,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def rank_filter(input, rank, size=None, footprint=None, output=None,
|
||||
mode="reflect", cval=0.0, origin=0, *, axes=None):
|
||||
"""Calculate a multidimensional rank filter.
|
||||
@@ -1535,7 +1513,6 @@ def rank_filter(input, rank, size=None, footprint=None, output=None,
|
||||
origin, 'rank', axes=axes)
|
||||
|
||||
|
||||
|
||||
def median_filter(input, size=None, footprint=None, output=None,
|
||||
mode="reflect", cval=0.0, origin=0, *, axes=None):
|
||||
"""
|
||||
@@ -1586,7 +1563,6 @@ def median_filter(input, size=None, footprint=None, output=None,
|
||||
origin, 'median', axes=axes)
|
||||
|
||||
|
||||
|
||||
def percentile_filter(input, percentile, size=None, footprint=None,
|
||||
output=None, mode="reflect", cval=0.0, origin=0, *,
|
||||
axes=None):
|
||||
@@ -1630,7 +1606,6 @@ def percentile_filter(input, percentile, size=None, footprint=None,
|
||||
cval, origin, 'percentile', axes=axes)
|
||||
|
||||
|
||||
|
||||
def generic_filter1d(input, function, filter_size, axis=-1,
|
||||
output=None, mode="reflect", cval=0.0, origin=0,
|
||||
extra_arguments=(), extra_keywords=None):
|
||||
@@ -1718,7 +1693,6 @@ def generic_filter1d(input, function, filter_size, axis=-1,
|
||||
return output
|
||||
|
||||
|
||||
|
||||
def generic_filter(input, function, size=None, footprint=None,
|
||||
output=None, mode="reflect", cval=0.0, origin=0,
|
||||
extra_arguments=(), extra_keywords=None):
|
||||
@@ -1846,4 +1820,4 @@ def generic_filter(input, function, size=None, footprint=None,
|
||||
mode = _ni_support._extend_mode_to_code(mode)
|
||||
_nd_image.generic_filter(input, function, footprint, output, mode,
|
||||
cval, origins, extra_arguments, extra_keywords)
|
||||
return output
|
||||
return output
|
||||
|
||||
@@ -11,7 +11,7 @@ def _prepad_for_spline_filter(input, mode, cval):
|
||||
npad = 12
|
||||
if mode == 'grid-constant':
|
||||
padded = np.pad(input, npad, mode='constant',
|
||||
constant_values=cval)
|
||||
constant_values=cval)
|
||||
elif mode == 'nearest':
|
||||
padded = np.pad(input, npad, mode='edge')
|
||||
else:
|
||||
@@ -21,6 +21,7 @@ def _prepad_for_spline_filter(input, mode, cval):
|
||||
padded = input
|
||||
return padded, npad
|
||||
|
||||
|
||||
def spline_filter1d(input, order=3, axis=-1, output=np.float64,
|
||||
mode='mirror'):
|
||||
"""
|
||||
|
||||
@@ -83,7 +83,8 @@ def _get_output(output, input, shape=None, complex_output=False):
|
||||
elif isinstance(output, (type, np.dtype)):
|
||||
# Classes (like `np.float32`) and dtypes are interpreted as dtype
|
||||
if complex_output and np.dtype(output).kind != 'c':
|
||||
warnings.warn("promoting specified output dtype to complex", stacklevel=3)
|
||||
warnings.warn(
|
||||
"promoting specified output dtype to complex", stacklevel=3)
|
||||
output = np.promote_types(output, np.complex64)
|
||||
output = np.zeros(shape, dtype=output)
|
||||
elif isinstance(output, str):
|
||||
|
||||
@@ -7,4 +7,4 @@ def normalize_axis_index(axis, ndim):
|
||||
|
||||
if axis < 0:
|
||||
axis = axis + ndim
|
||||
return axis
|
||||
return axis
|
||||
|
||||
@@ -3,6 +3,7 @@ import numpy as np
|
||||
from .. import scipy as ndi
|
||||
from ._utils import convert_to_float
|
||||
|
||||
|
||||
def _clip_warp_output(input_image, output_image, mode, cval, clip):
|
||||
"""Clip output image to range of values of input image.
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ _integer_types = (
|
||||
np.uintp,
|
||||
np.uintc,
|
||||
)
|
||||
_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max) for t in _integer_types}
|
||||
_integer_ranges = {t: (np.iinfo(t).min, np.iinfo(t).max)
|
||||
for t in _integer_types}
|
||||
dtype_range = {
|
||||
bool: (False, True),
|
||||
np.bool_: (False, True),
|
||||
@@ -175,7 +176,8 @@ def _scale(a, n, m, copy=True):
|
||||
# downscale with precision loss
|
||||
if copy:
|
||||
b = np.empty(a.shape, _dtype_bits(kind, m))
|
||||
np.floor_divide(a, 2 ** (n - m), out=b, dtype=a.dtype, casting='unsafe')
|
||||
np.floor_divide(a, 2 ** (n - m), out=b,
|
||||
dtype=a.dtype, casting='unsafe')
|
||||
return b
|
||||
else:
|
||||
a //= 2 ** (n - m)
|
||||
@@ -282,7 +284,8 @@ def _convert(image, dtype, force_copy=False, uniform=False):
|
||||
return image
|
||||
|
||||
if not (dtype_in in _supported_types and dtype_out in _supported_types):
|
||||
raise ValueError(f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.')
|
||||
raise ValueError(
|
||||
f'Cannot convert from {dtypeobj_in} to ' f'{dtypeobj_out}.')
|
||||
|
||||
if kind_in in 'ui':
|
||||
imin_in = np.iinfo(dtype_in).min
|
||||
@@ -318,7 +321,8 @@ def _convert(image, dtype, force_copy=False, uniform=False):
|
||||
|
||||
if not uniform:
|
||||
if kind_out == 'u':
|
||||
image_out = np.multiply(image, imax_out, dtype=computation_type)
|
||||
image_out = np.multiply(
|
||||
image, imax_out, dtype=computation_type)
|
||||
else:
|
||||
image_out = np.multiply(
|
||||
image, (imax_out - imin_out) / 2, dtype=computation_type
|
||||
@@ -327,7 +331,8 @@ def _convert(image, dtype, force_copy=False, uniform=False):
|
||||
np.rint(image_out, out=image_out)
|
||||
np.clip(image_out, imin_out, imax_out, out=image_out)
|
||||
elif kind_out == 'u':
|
||||
image_out = np.multiply(image, imax_out + 1, dtype=computation_type)
|
||||
image_out = np.multiply(
|
||||
image, imax_out + 1, dtype=computation_type)
|
||||
np.clip(image_out, 0, imax_out, out=image_out)
|
||||
else:
|
||||
image_out = np.multiply(
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
from ast import literal_eval
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from ..constants import LATEST_NODE_LIB_PATH, NODE_LIB_DIRPATH, VERSIONS
|
||||
from ..utils import get_cache_dir
|
||||
|
||||
|
||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||
node.location.x = target_node.location.x + offset[0]
|
||||
node.location.y = target_node.location.y + offset[1]
|
||||
|
||||
|
||||
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
||||
xs = []
|
||||
ys = []
|
||||
for target_node in target_nodes:
|
||||
xs.append(target_node.location.x)
|
||||
ys.append(target_node.location.y)
|
||||
|
||||
node.location.x = sum(xs) / len(xs) + offset[0]
|
||||
node.location.y = sum(ys) / len(ys) + offset[1]
|
||||
|
||||
|
||||
def get_node_type(node):
|
||||
node_type = type(node).__name__
|
||||
if node_type == "GeometryNodeGroup":
|
||||
node_type = node.node_tree.name
|
||||
|
||||
return node_type
|
||||
|
||||
|
||||
def get_nodes_by_type(node_group, type_name: str):
|
||||
return [node for node in node_group.nodes if get_node_type(node) == type_name]
|
||||
|
||||
|
||||
def get_container_objs_from_selection():
|
||||
container_objs = []
|
||||
for obj in bpy.context.selected_objects:
|
||||
if get_container_obj(obj):
|
||||
container_objs.append(obj)
|
||||
|
||||
return list(set(container_objs))
|
||||
|
||||
|
||||
def get_container_obj(current_obj):
|
||||
if current_obj:
|
||||
if current_obj.get('bioxel_container'):
|
||||
return current_obj
|
||||
elif current_obj.get('bioxel_layer'):
|
||||
parent = current_obj.parent
|
||||
return parent if parent.get('bioxel_container') else None
|
||||
return None
|
||||
|
||||
|
||||
def get_layer_prop_value(layer_obj: bpy.types.Object, prop_name: str):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
prop = layer_node.inputs.get(prop_name)
|
||||
if prop is None:
|
||||
return None
|
||||
|
||||
value = prop.default_value
|
||||
|
||||
if type(value).__name__ == "bpy_prop_array":
|
||||
value = tuple(value)
|
||||
return tuple([int(v) for v in value]) \
|
||||
if prop in ["shape"] else value
|
||||
elif type(value).__name__ == "str":
|
||||
return str(value)
|
||||
elif type(value).__name__ == "float":
|
||||
value = float(value)
|
||||
return round(value, 2) \
|
||||
if prop in ["bioxel_size"] else value
|
||||
elif type(value).__name__ == "int":
|
||||
value = int(value)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def get_layer_name(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "name")
|
||||
|
||||
|
||||
def get_layer_kind(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "kind")
|
||||
|
||||
|
||||
def get_layer_label(layer_obj):
|
||||
name = get_layer_name(layer_obj)
|
||||
# kind = get_layer_kind(layer_obj)
|
||||
|
||||
label = f"{name}"
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
return "**MISSING**" + label
|
||||
elif is_temp_layer(layer_obj):
|
||||
return "* " + label
|
||||
else:
|
||||
return label
|
||||
|
||||
|
||||
def is_missing_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
return not cache_filepath.is_file()
|
||||
|
||||
|
||||
def is_temp_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
cache_dirpath = Path(get_cache_dir())
|
||||
return cache_dirpath in cache_filepath.parents
|
||||
|
||||
|
||||
def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
layer_node.inputs[prop].default_value = value
|
||||
|
||||
|
||||
def get_layer_obj(current_obj: bpy.types.Object):
|
||||
if current_obj:
|
||||
if current_obj.get('bioxel_layer') and current_obj.parent:
|
||||
if current_obj.parent.get('bioxel_container'):
|
||||
return current_obj
|
||||
return None
|
||||
|
||||
|
||||
def get_container_layer_objs(container_obj: bpy.types.Object):
|
||||
layer_objs = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.parent == container_obj and get_layer_obj(obj):
|
||||
layer_objs.append(obj)
|
||||
|
||||
return layer_objs
|
||||
|
||||
|
||||
def get_all_layer_objs():
|
||||
layer_objs = []
|
||||
for obj in bpy.data.objects:
|
||||
if get_layer_obj(obj):
|
||||
layer_objs.append(obj)
|
||||
|
||||
return layer_objs
|
||||
|
||||
|
||||
def add_driver(target, target_prop, var_sources, expression):
|
||||
driver = target.driver_add(target_prop)
|
||||
is_vector = isinstance(driver, list)
|
||||
drivers = driver if is_vector else [driver]
|
||||
|
||||
for i, driver in enumerate(drivers):
|
||||
for j, var_source in enumerate(var_sources):
|
||||
|
||||
source = var_source['source']
|
||||
prop = var_source['prop']
|
||||
|
||||
var = driver.driver.variables.new()
|
||||
var.name = f"var{j}"
|
||||
|
||||
var.targets[0].id_type = source.id_type
|
||||
var.targets[0].id = source
|
||||
var.targets[0].data_path = f'["{prop}"][{i}]'\
|
||||
if is_vector else f'["{prop}"]'
|
||||
|
||||
driver.driver.expression = expression
|
||||
|
||||
|
||||
def add_direct_driver(target, target_prop, source, source_prop):
|
||||
drivers = [
|
||||
{
|
||||
"source": source,
|
||||
"prop": source_prop
|
||||
}
|
||||
]
|
||||
expression = "var0"
|
||||
add_driver(target, target_prop, drivers, expression)
|
||||
|
||||
|
||||
def read_file_prop(content: str):
|
||||
props = {}
|
||||
for line in content.split("\n"):
|
||||
line = line.replace(" ", "")
|
||||
p = line.split("=")[0]
|
||||
if p != "":
|
||||
v = line.split("=")[-1]
|
||||
props[p] = v
|
||||
return props
|
||||
|
||||
|
||||
def write_file_prop(props: dict):
|
||||
lines = []
|
||||
for p, v in props.items():
|
||||
lines.append(f"{p} = {v}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def set_file_prop(prop, value):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
props[prop] = value
|
||||
bpy.data.texts["BioxelNodes"].clear()
|
||||
bpy.data.texts["BioxelNodes"].write(write_file_prop(props))
|
||||
|
||||
|
||||
def get_file_prop(prop):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
return props.get(prop)
|
||||
|
||||
|
||||
def get_node_version():
|
||||
node_version = get_file_prop("node_version")
|
||||
return literal_eval(node_version) if node_version else None
|
||||
|
||||
|
||||
def is_incompatible():
|
||||
node_version = get_node_version()
|
||||
if node_version is None:
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
return True
|
||||
else:
|
||||
addon_version = VERSIONS[0]["node_version"]
|
||||
if node_version[0] != addon_version[0]\
|
||||
or node_version[1] != addon_version[1]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_node_lib_path(node_version):
|
||||
version_str = "v"+".".join([str(i) for i in list(node_version)])
|
||||
lib_filename = f"BioxelNodes_{version_str}.blend"
|
||||
return Path(NODE_LIB_DIRPATH,
|
||||
lib_filename).resolve()
|
||||
|
||||
|
||||
def local_lib_not_updated():
|
||||
addon_version = VERSIONS[0]["node_version"]
|
||||
addon_lib_path = LATEST_NODE_LIB_PATH
|
||||
|
||||
use_local = False
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
lib = node_group.library
|
||||
if lib:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
if lib_path != addon_lib_path:
|
||||
use_local = True
|
||||
break
|
||||
|
||||
not_update = get_node_version() != addon_version
|
||||
return use_local and not_update
|
||||
|
||||
|
||||
def get_output_node(node_group):
|
||||
try:
|
||||
output_node = get_nodes_by_type(node_group,
|
||||
'NodeGroupOutput')[0]
|
||||
except:
|
||||
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
return output_node
|
||||
@@ -1,186 +0,0 @@
|
||||
import bpy
|
||||
|
||||
from ..bioxel.container import Container
|
||||
from bpy_extras.io_utils import axis_conversion
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
|
||||
from .layer import Layer, layer_to_obj, obj_to_layer
|
||||
from .common import (get_container_layer_objs,
|
||||
get_layer_prop_value,
|
||||
get_nodes_by_type, get_output_node,
|
||||
move_node_to_node)
|
||||
from .node import add_node_to_graph
|
||||
from ..utils import get_use_link
|
||||
|
||||
NODE_TYPE = {
|
||||
"label": "BioxelNodes_MaskByLabel",
|
||||
"scalar": "BioxelNodes_MaskByThreshold"
|
||||
}
|
||||
|
||||
|
||||
def calc_bbox_verts(origin: tuple, size: tuple):
|
||||
bbox_origin = Vector(
|
||||
(origin[0], origin[1], origin[2]))
|
||||
bbox_size = Vector(
|
||||
(size[0], size[1], size[2]))
|
||||
bbox_verts = [
|
||||
(
|
||||
bbox_origin[0] + 0,
|
||||
bbox_origin[1] + 0,
|
||||
bbox_origin[2] + 0
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + 0,
|
||||
bbox_origin[1] + 0,
|
||||
bbox_origin[2] + bbox_size[2]
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + 0,
|
||||
bbox_origin[1] + bbox_size[1],
|
||||
bbox_origin[2] + 0
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + 0,
|
||||
bbox_origin[1] + bbox_size[1],
|
||||
bbox_origin[2] + bbox_size[2]
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + bbox_size[0],
|
||||
bbox_origin[1] + 0,
|
||||
bbox_origin[2] + 0
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + bbox_size[0],
|
||||
bbox_origin[1] + 0,
|
||||
bbox_origin[2] + bbox_size[2],
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + bbox_size[0],
|
||||
bbox_origin[1] + bbox_size[1],
|
||||
bbox_origin[2] + 0,
|
||||
),
|
||||
(
|
||||
bbox_origin[0] + bbox_size[0],
|
||||
bbox_origin[1] + bbox_size[1],
|
||||
bbox_origin[2] + bbox_size[2],
|
||||
),
|
||||
]
|
||||
return bbox_verts
|
||||
|
||||
|
||||
def obj_to_container(container_obj: bpy.types.Object):
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
layers = [obj_to_layer(obj) for obj in layer_objs]
|
||||
container = Container(name=container_obj.name,
|
||||
layers=layers)
|
||||
return container
|
||||
|
||||
|
||||
def add_layers(layers: list[Layer],
|
||||
container_obj: bpy.types.Object,
|
||||
cache_dir: str):
|
||||
|
||||
node_group = container_obj.modifiers[0].node_group
|
||||
output_node = get_output_node(node_group)
|
||||
|
||||
for i, layer in enumerate(layers):
|
||||
layer_obj = layer_to_obj(layer, container_obj, cache_dir)
|
||||
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
|
||||
|
||||
if len(output_node.inputs[0].links) == 0:
|
||||
node_group.links.new(fetch_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
move_node_to_node(fetch_node, output_node, (-900, 0))
|
||||
else:
|
||||
move_node_to_node(fetch_node, output_node, (0, -100 * (i+1)))
|
||||
|
||||
return container_obj
|
||||
|
||||
|
||||
def container_to_obj(container: Container,
|
||||
scene_scale: float,
|
||||
step_size: float,
|
||||
cache_dir: str):
|
||||
# Wrapper a Container
|
||||
|
||||
# Make transformation
|
||||
# (S)uperior -Z -> Y
|
||||
# (A)osterior Y -> Z
|
||||
mat_ras2blender = axis_conversion(from_forward='-Z',
|
||||
from_up='Y',
|
||||
to_forward='Y',
|
||||
to_up='Z').to_4x4()
|
||||
|
||||
mat_scene_scale = Matrix.Scale(scene_scale,
|
||||
4)
|
||||
|
||||
bpy.ops.mesh.primitive_cube_add(enter_editmode=False,
|
||||
align='WORLD',
|
||||
location=(0, 0, 0),
|
||||
scale=(1, 1, 1))
|
||||
|
||||
container_obj = bpy.context.active_object
|
||||
|
||||
bbox_verts = calc_bbox_verts((0, 0, 0), container.layers[0].shape)
|
||||
for i, vert in enumerate(container_obj.data.vertices):
|
||||
transform = Matrix(container.layers[0].affine)
|
||||
vert.co = transform @ Vector(bbox_verts[i])
|
||||
|
||||
container_obj.matrix_world = mat_ras2blender @ mat_scene_scale
|
||||
container_obj.name = container.name
|
||||
container_obj.data.name = container.name
|
||||
container_obj.show_in_front = True
|
||||
|
||||
container_obj.lock_location[0] = True
|
||||
container_obj.lock_location[1] = True
|
||||
container_obj.lock_location[2] = True
|
||||
|
||||
container_obj.lock_rotation[0] = True
|
||||
container_obj.lock_rotation[1] = True
|
||||
container_obj.lock_rotation[2] = True
|
||||
|
||||
container_obj.lock_scale[0] = True
|
||||
container_obj.lock_scale[1] = True
|
||||
container_obj.lock_scale[2] = True
|
||||
|
||||
container_obj['bioxel_container'] = True
|
||||
container_obj["scene_scale"] = scene_scale
|
||||
container_obj["step_size"] = step_size
|
||||
|
||||
modifier = container_obj.modifiers.new("GeometryNodes", 'NODES')
|
||||
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
|
||||
node_group.interface.new_socket(name="Output",
|
||||
in_out="OUTPUT",
|
||||
socket_type="NodeSocketGeometry")
|
||||
modifier.node_group = node_group
|
||||
node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
container_obj = add_layers(container.layers,
|
||||
container_obj=container_obj,
|
||||
cache_dir=cache_dir)
|
||||
|
||||
try:
|
||||
layer_node = get_nodes_by_type(
|
||||
node_group, "BioxelNodes_FetchLayer")[0]
|
||||
center_node = add_node_to_graph("ReCenter",
|
||||
node_group,
|
||||
use_link=get_use_link())
|
||||
|
||||
output_node = get_output_node(node_group)
|
||||
|
||||
node_group.links.new(layer_node.outputs[0],
|
||||
center_node.inputs[0])
|
||||
node_group.links.new(center_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
|
||||
move_node_to_node(center_node, layer_node, (300, 0))
|
||||
bpy.ops.bioxelnodes.add_slicer('EXEC_DEFAULT')
|
||||
except:
|
||||
pass
|
||||
|
||||
return container_obj
|
||||
@@ -1,215 +0,0 @@
|
||||
import random
|
||||
import re
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import pyopenvdb as vdb
|
||||
except:
|
||||
import openvdb as vdb
|
||||
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from ..bioxel.layer import Layer
|
||||
from ..utils import get_use_link
|
||||
from .node import add_node_to_graph
|
||||
from .common import (get_layer_prop_value,
|
||||
move_node_between_nodes)
|
||||
|
||||
|
||||
def obj_to_layer(layer_obj: bpy.types.Object):
|
||||
cache_filepath = Path(bpy.path.abspath(layer_obj.data.filepath)).resolve()
|
||||
is_sequence = re.search(r'\.\d{4}\.',
|
||||
cache_filepath.name) is not None
|
||||
if is_sequence:
|
||||
cache_path = cache_filepath.parent
|
||||
data_frames = ()
|
||||
for f in cache_path.iterdir():
|
||||
if not f.is_file() or f.suffix != ".vdb":
|
||||
continue
|
||||
grids, base_metadata = vdb.readAll(str(f))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data_frame = np.ndarray(data_shape, np.float32)
|
||||
grid.copyToArray(data_frame)
|
||||
data_frames += (data_frame,)
|
||||
data = np.stack(data_frames)
|
||||
else:
|
||||
grids, base_metadata = vdb.readAll(str(cache_filepath))
|
||||
grid = grids[0]
|
||||
metadata = grid.metadata
|
||||
if grid["layer_kind"] in ['label', 'scalar']:
|
||||
data_shape = grid["data_shape"]
|
||||
else:
|
||||
data_shape = tuple(list(grid["data_shape"]) + [3])
|
||||
data = np.ndarray(data_shape, np.float32)
|
||||
grid.copyToArray(data)
|
||||
data = np.expand_dims(data, axis=0) # expend frame
|
||||
|
||||
name = get_layer_prop_value(layer_obj, "name") \
|
||||
or metadata["layer_name"]
|
||||
kind = get_layer_prop_value(layer_obj, "kind") \
|
||||
or metadata["layer_kind"]
|
||||
affine = metadata["layer_affine"]
|
||||
dtype = get_layer_prop_value(layer_obj, "dtype") \
|
||||
or metadata.get("data_dtype") or "float32"
|
||||
offset = get_layer_prop_value(layer_obj, "offset") \
|
||||
or metadata.get("data_offset") or 0
|
||||
|
||||
data = data - np.full_like(data, offset)
|
||||
data = data.astype(dtype)
|
||||
|
||||
if kind in ["scalar", "label"]:
|
||||
data = np.expand_dims(data, axis=-1) # expend channel
|
||||
|
||||
layer = Layer(data=data,
|
||||
name=name,
|
||||
kind=kind,
|
||||
affine=affine)
|
||||
|
||||
return layer
|
||||
|
||||
|
||||
def layer_to_obj(layer: Layer,
|
||||
container_obj: bpy.types.Object,
|
||||
cache_dir: str):
|
||||
|
||||
data = layer.data
|
||||
|
||||
# TXYZC > TXYZ
|
||||
if layer.kind in ['label', 'scalar']:
|
||||
data = np.amax(data, -1)
|
||||
|
||||
offset = 0
|
||||
if layer.kind in ['scalar']:
|
||||
data = data.astype(np.float32)
|
||||
orig_min = float(np.min(data))
|
||||
if orig_min < 0:
|
||||
offset = -orig_min
|
||||
|
||||
data = data + np.full_like(data, offset)
|
||||
|
||||
metadata = {
|
||||
"layer_name": layer.name,
|
||||
"layer_kind": layer.kind,
|
||||
"layer_affine": layer.affine.tolist(),
|
||||
"data_shape": layer.shape,
|
||||
"data_dtype": layer.data.dtype.str,
|
||||
"data_offset": offset
|
||||
}
|
||||
|
||||
layer_display_name = f"{container_obj.name}_{layer.name}"
|
||||
if layer.frame_count > 1:
|
||||
print(f"Saving the Cache of {layer.name}...")
|
||||
vdb_name = str(uuid4())
|
||||
sequence_path = Path(cache_dir, vdb_name)
|
||||
sequence_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cache_filepaths = []
|
||||
for f in range(layer.frame_count):
|
||||
if layer.kind in ['label', 'scalar']:
|
||||
grid = vdb.FloatGrid()
|
||||
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
|
||||
else:
|
||||
# color
|
||||
grid = vdb.Vec3SGrid()
|
||||
grid.copyFromArray(
|
||||
data[f, :, :, :, :].copy().astype(np.float32))
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
layer.affine.transpose())
|
||||
grid.metadata = metadata
|
||||
grid.name = layer.kind
|
||||
|
||||
cache_filepath = Path(sequence_path,
|
||||
f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
|
||||
vdb.write(str(cache_filepath), grids=[grid])
|
||||
cache_filepaths.append(cache_filepath)
|
||||
|
||||
else:
|
||||
if layer.kind in ['label', 'scalar']:
|
||||
grid = vdb.FloatGrid()
|
||||
grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32))
|
||||
else:
|
||||
# color
|
||||
grid = vdb.Vec3SGrid()
|
||||
grid.copyFromArray(data[0, :, :, :, :].copy().astype(np.float32))
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
layer.affine.transpose())
|
||||
grid.metadata = metadata
|
||||
grid.name = layer.kind
|
||||
|
||||
print(f"Saving the Cache of {layer.name}...")
|
||||
cache_filepath = Path(cache_dir, f"{uuid4()}.vdb")
|
||||
vdb.write(str(cache_filepath), grids=[grid])
|
||||
cache_filepaths = [cache_filepath]
|
||||
|
||||
layer_data = bpy.data.volumes.new(layer_display_name)
|
||||
layer_data.render.space = 'WORLD'
|
||||
scene_scale = container_obj.get("scene_scale") or 0.01
|
||||
step_size = container_obj.get("step_size") or 1
|
||||
layer_data.render.step_size = scene_scale * step_size
|
||||
layer_data.sequence_mode = 'REPEAT'
|
||||
layer_data.filepath = str(cache_filepaths[0])
|
||||
|
||||
if layer.frame_count > 1:
|
||||
layer_data.is_sequence = True
|
||||
layer_data.frame_duration = layer.frame_count
|
||||
else:
|
||||
layer_data.is_sequence = False
|
||||
|
||||
layer_obj = bpy.data.objects.new(layer_display_name, layer_data)
|
||||
layer_obj['bioxel_layer'] = True
|
||||
|
||||
print(f"Creating Node for {layer.name}...")
|
||||
modifier = layer_obj.modifiers.new("GeometryNodes", 'NODES')
|
||||
node_group = bpy.data.node_groups.new('GeometryNodes', 'GeometryNodeTree')
|
||||
node_group.interface.new_socket(name="Cache",
|
||||
in_out="INPUT",
|
||||
socket_type="NodeSocketGeometry")
|
||||
node_group.interface.new_socket(name="Layer",
|
||||
in_out="OUTPUT",
|
||||
socket_type="NodeSocketGeometry")
|
||||
modifier.node_group = node_group
|
||||
|
||||
layer_node = add_node_to_graph("_Layer",
|
||||
node_group,
|
||||
use_link=get_use_link())
|
||||
|
||||
layer_node.inputs['name'].default_value = layer.name
|
||||
layer_node.inputs['shape'].default_value = layer.shape
|
||||
layer_node.inputs['kind'].default_value = layer.kind
|
||||
|
||||
for i in range(layer.affine.shape[1]):
|
||||
for j in range(layer.affine.shape[0]):
|
||||
affine_key = f"affine{i}{j}"
|
||||
layer_node.inputs[affine_key].default_value = layer.affine[j, i]
|
||||
|
||||
layer_node.inputs['unique'].default_value = random.uniform(0, 1)
|
||||
layer_node.inputs['bioxel_size'].default_value = layer.bioxel_size[0]
|
||||
layer_node.inputs['dtype'].default_value = layer.dtype.str
|
||||
layer_node.inputs['dtype_num'].default_value = layer.dtype.num
|
||||
layer_node.inputs['frame_count'].default_value = layer.frame_count
|
||||
layer_node.inputs['channel_count'].default_value = layer.channel_count
|
||||
layer_node.inputs['offset'].default_value = max(0, -layer.min)
|
||||
layer_node.inputs['min'].default_value = layer.min
|
||||
layer_node.inputs['max'].default_value = layer.max
|
||||
|
||||
input_node = node_group.nodes.new("NodeGroupInput")
|
||||
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
node_group.links.new(input_node.outputs[0],
|
||||
layer_node.inputs[0])
|
||||
node_group.links.new(layer_node.outputs[0],
|
||||
output_node.inputs[0])
|
||||
|
||||
move_node_between_nodes(
|
||||
layer_node, [input_node, output_node])
|
||||
|
||||
layer_obj.parent = container_obj
|
||||
|
||||
return layer_obj
|
||||
@@ -1,71 +0,0 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from .common import get_file_prop, set_file_prop
|
||||
from ..exceptions import NoFound
|
||||
|
||||
from ..constants import LATEST_NODE_LIB_PATH, VERSIONS
|
||||
|
||||
|
||||
def get_node_tree(node_type: str, use_link=True):
|
||||
# unannotate below for local debug in node lib file.
|
||||
# node_group = bpy.data.node_groups[node_type]
|
||||
# return node_group
|
||||
|
||||
# added node is always from latest node version
|
||||
addon_version = VERSIONS[0]["node_version"]
|
||||
addon_lib_path = LATEST_NODE_LIB_PATH
|
||||
|
||||
if get_file_prop("node_version") is None:
|
||||
set_file_prop("node_version", addon_version)
|
||||
|
||||
local_lib = None
|
||||
for node_group in bpy.data.node_groups:
|
||||
if node_group.name.startswith("BioxelNodes"):
|
||||
lib = node_group.library
|
||||
if lib:
|
||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
||||
if lib_path != addon_lib_path:
|
||||
local_lib = lib.filepath
|
||||
break
|
||||
|
||||
# local lib first
|
||||
lib_path_str = local_lib or str(addon_lib_path)
|
||||
|
||||
with bpy.data.libraries.load(lib_path_str,
|
||||
link=use_link,
|
||||
relative=True) as (data_from, data_to):
|
||||
data_to.node_groups = [n for n in data_from.node_groups
|
||||
if n == node_type]
|
||||
|
||||
node_tree = data_to.node_groups[0]
|
||||
|
||||
if node_tree is None:
|
||||
raise NoFound('No custom node found')
|
||||
|
||||
return node_tree
|
||||
|
||||
|
||||
def assign_node_tree(node, node_tree):
|
||||
node.node_tree = node_tree
|
||||
node.width = 200.0
|
||||
node.name = node_tree.name
|
||||
return node
|
||||
|
||||
|
||||
def add_node_to_graph(node_name: str, node_group, node_label=None, use_link=True):
|
||||
node_type = f"BioxelNodes_{node_name}"
|
||||
node_label = node_label or node_name
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in node_group.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
node_tree = get_node_tree(node_type, use_link)
|
||||
node = node_group.nodes.new("GeometryNodeGroup")
|
||||
assign_node_tree(node, node_tree)
|
||||
|
||||
node.label = node_label
|
||||
node.show_options = False
|
||||
return node
|
||||
@@ -1,7 +1,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "bioxelnodes"
|
||||
version = "1.0.9"
|
||||
version = "2.0.0"
|
||||
name = "Bioxel Nodes"
|
||||
tagline = "For scientific volumetric data visualization in Blender"
|
||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||
@@ -9,12 +9,30 @@ type = "add-on"
|
||||
website = "https://omoolab.github.io/BioxelNodes/latest"
|
||||
|
||||
tags = ["Geometry Nodes", "Render", "Import-Export"]
|
||||
blender_version_min = "4.2.0"
|
||||
blender_version_min = "5.0.0"
|
||||
license = ['SPDX:GPL-3.0-or-later']
|
||||
copyright = ["2024 OmooLab"]
|
||||
platforms = ["windows-x64"]
|
||||
|
||||
wheels = ["./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl", "./wheels/lxml-5.3.2-cp311-cp311-win_amd64.whl", "./wheels/mrcfile-1.5.1-py2.py3-none-any.whl", "./wheels/pyometiff-1.0.0-py3-none-any.whl", "./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl", "./wheels/tifffile-2024.7.24-py3-none-any.whl", "./wheels/transforms3d-0.4.2-py3-none-any.whl"]
|
||||
wheels = [
|
||||
"./wheels/contourpy-1.3.3-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/cycler-0.12.1-py3-none-any.whl",
|
||||
"./wheels/fonttools-4.60.1-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/lxml-6.0.2-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/matplotlib-3.10.7-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/mrcfile-1.5.1-py2.py3-none-any.whl",
|
||||
"./wheels/packaging-25.0-py3-none-any.whl",
|
||||
"./wheels/pillow-11.2.1-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/pyometiff-1.0.0-py3-none-any.whl",
|
||||
"./wheels/pyparsing-3.2.5-py3-none-any.whl",
|
||||
"./wheels/python_dateutil-2.9.0.post0-py2.py3-none-any.whl",
|
||||
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl",
|
||||
"./wheels/six-1.17.0-py2.py3-none-any.whl",
|
||||
"./wheels/tifffile-2024.7.24-py3-none-any.whl",
|
||||
"./wheels/transforms3d-0.4.2-py3-none-any.whl",
|
||||
]
|
||||
|
||||
[permissions]
|
||||
files = "Import/export volume data from/to disk"
|
||||
files = "Import/export volume data from/to disk"
|
||||
|
||||
@@ -1,257 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
VERSIONS = [{"label": "Latest", "node_version": (1, 0, 7)},
|
||||
{"label": "v0.3.x", "node_version": (0, 3, 3)},
|
||||
{"label": "v0.2.x", "node_version": (0, 2, 9)}]
|
||||
|
||||
NODE_LIB_DIRPATH = Path(Path(__file__).parent,
|
||||
"assets/Nodes").resolve()
|
||||
"assets/O Bioxel").resolve()
|
||||
|
||||
LATEST_NODE_LIB_PATH = lib_path = Path(NODE_LIB_DIRPATH,
|
||||
"BioxelNodes_latest.blend").resolve()
|
||||
LATEST_NODE_LIB_PATH = Path(NODE_LIB_DIRPATH,
|
||||
"BioxelNodes.blend").resolve()
|
||||
|
||||
COMPONENT_OUTPUT_NODES = [
|
||||
"CutoutByThreshold",
|
||||
"CutoutByRange",
|
||||
"CutoutByHue",
|
||||
"JoinComponent",
|
||||
"SetProperties",
|
||||
"SetColor",
|
||||
"SetColorByColor",
|
||||
"SetColorByColor",
|
||||
"SetColorByRamp2",
|
||||
"SetColorByRamp3",
|
||||
"SetColorByRamp4",
|
||||
"SetColorByRamp5",
|
||||
"Cut",
|
||||
]
|
||||
|
||||
MENU_ITEMS = [
|
||||
{
|
||||
'label': 'Component',
|
||||
'icon': 'OUTLINER_DATA_VOLUME',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Cutout by Threshold',
|
||||
'icon': 'EMPTY_SINGLE_ARROW',
|
||||
'name': 'CutoutByThreshold',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Range',
|
||||
'icon': 'IPO_CONSTANT',
|
||||
'name': 'CutoutByRange',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Cutout by Hue',
|
||||
'icon': 'COLOR',
|
||||
'name': 'CutoutByHue',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'To Surface',
|
||||
'icon': 'MESH_DATA',
|
||||
'name': 'ToSurface',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Join Component',
|
||||
'icon': 'CONSTRAINT_BONE',
|
||||
'name': 'JoinComponent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slice',
|
||||
'icon': 'TEXTURE',
|
||||
'name': 'Slice',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Property',
|
||||
'icon': 'PROPERTIES',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Set Properties',
|
||||
'icon': 'PROPERTIES',
|
||||
'name': 'SetProperties',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Set Color',
|
||||
'icon': 'IPO_SINE',
|
||||
'name': 'SetColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Color',
|
||||
'icon': 'IPO_QUINT',
|
||||
'name': 'SetColorByColor',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 2',
|
||||
'icon': 'IPO_QUAD',
|
||||
'name': 'SetColorByRamp2',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 3',
|
||||
'icon': 'IPO_CUBIC',
|
||||
'name': 'SetColorByRamp3',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 4',
|
||||
'icon': 'IPO_QUART',
|
||||
'name': 'SetColorByRamp4',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Set Color by Ramp 5',
|
||||
'icon': 'IPO_QUINT',
|
||||
'name': 'SetColorByRamp5',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Surface',
|
||||
'icon': 'MESH_DATA',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Membrane Shader',
|
||||
'icon': 'NODE_MATERIAL',
|
||||
'name': 'AssignMembraneShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Solid Shader',
|
||||
'icon': 'SHADING_SOLID',
|
||||
'name': 'AssignSolidShader',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Slime Shader',
|
||||
'icon': 'OUTLINER_DATA_META',
|
||||
'name': 'AssignSlimeShader',
|
||||
'description': ''
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'name': 'Transform',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Transform Parent',
|
||||
'icon': 'ORIENTATION_PARENT',
|
||||
'name': 'TransformParent',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'ReCenter',
|
||||
'icon': 'PROP_CON',
|
||||
'name': 'ReCenter',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Copy Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'name': 'CopyTransform',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Extract Transform',
|
||||
'icon': 'EMPTY_AXIS',
|
||||
'name': 'ExtractTransform',
|
||||
'description': ''
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Cut',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Cut',
|
||||
'icon': 'MOD_BEVEL',
|
||||
'name': 'Cut',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Primitive Cutter',
|
||||
'icon': 'MOD_LINEART',
|
||||
'name': 'PrimitiveCutter',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Object Cutter',
|
||||
'icon': 'MESH_PLANE',
|
||||
'name': 'ObjectCutter',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'label': 'Extra',
|
||||
'icon': 'MODIFIER',
|
||||
'items': [
|
||||
{
|
||||
'label': 'Fetch Mesh',
|
||||
'icon': 'OUTLINER_OB_MESH',
|
||||
'name': 'FetchMesh',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Fetch Volume',
|
||||
'icon': 'OUTLINER_OB_VOLUME',
|
||||
'name': 'FetchVolume',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Fetch Shape Wire',
|
||||
'icon': 'FILE_VOLUME',
|
||||
'name': 'FetchShapeWire',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Fetch Bbox Wire',
|
||||
'icon': 'MESH_CUBE',
|
||||
'name': 'FetchBboxWire',
|
||||
'description': ''
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
'label': 'Inflate',
|
||||
'icon': 'OUTLINER_OB_META',
|
||||
'name': 'Inflate',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Smooth',
|
||||
'icon': 'MOD_SMOOTH',
|
||||
'name': 'Smooth',
|
||||
'description': ''
|
||||
},
|
||||
{
|
||||
'label': 'Remove Small Island',
|
||||
'icon': 'FORCE_LENNARDJONES',
|
||||
'name': 'RemoveSmallIsland',
|
||||
'description': ''
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
PREVIEW_COLLECTIONS = {}
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
class CancelledByUser(Exception):
|
||||
def __init__(self):
|
||||
message = 'Cancelled by user'
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class NoContent(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class NoFound(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
|
||||
class Incompatible(Exception):
|
||||
def __init__(self, message):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
# This file is kept for backward compatibility
|
||||
# Custom exceptions have been replaced with built-in exceptions:
|
||||
# CancelledByUser -> CancelledError
|
||||
# NoContent -> ValueError
|
||||
# NoFound -> LookupError
|
||||
# Incompatible -> ValueError
|
||||
|
||||
277
src/bioxelnodes/layer.py
Normal file
277
src/bioxelnodes/layer.py
Normal file
@@ -0,0 +1,277 @@
|
||||
import json
|
||||
import time
|
||||
from typing import Any, List, Dict
|
||||
from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import openvdb as vdb
|
||||
|
||||
from .bioxel.layer import Layer
|
||||
from .utils import ndarray_to_png
|
||||
|
||||
LAYERS_JSON = "bioxel_layers"
|
||||
|
||||
|
||||
def cache_layer_data(layer: Layer, cache_path: str):
|
||||
"""
|
||||
Cache the given Layer's data as one or more VDB files.
|
||||
|
||||
- For multi-frame layers, writes per-frame VDB files named data.0001.vdb, data.0002.vdb, ...
|
||||
- For single-frame layers, writes a single data.vdb file.
|
||||
- Applies basic type handling:
|
||||
- label/scalar layers collapse channel dimension via max.
|
||||
- scalar layers are offset to avoid negative values.
|
||||
- The VDB grids will have their transform set from layer.affine but no additional metadata is written.
|
||||
|
||||
Parameters:
|
||||
- layer: Layer object containing ndarray data and metadata.
|
||||
- cache_path: directory path where VDB files will be written (created if missing).
|
||||
"""
|
||||
data = layer.data
|
||||
|
||||
# 处理标量/标签类型(去除通道维度)
|
||||
if layer.kind in ["label", "scalar"]:
|
||||
data = np.amax(data, -1)
|
||||
|
||||
# 标量类型偏移处理(避免负值)
|
||||
offset = 0
|
||||
if layer.kind in ["scalar"]:
|
||||
data = data.astype(np.float32)
|
||||
orig_min = float(np.min(data))
|
||||
if orig_min < 0:
|
||||
offset = -orig_min
|
||||
data = data + np.full_like(data, offset)
|
||||
|
||||
# 创建缓存目录
|
||||
cache_path = Path(cache_path)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 多帧序列处理
|
||||
if layer.frame_count > 1:
|
||||
for f in range(layer.frame_count):
|
||||
# 根据图层类型创建VDB网格
|
||||
if layer.kind in ["label", "scalar"]:
|
||||
grid = vdb.FloatGrid()
|
||||
grid.copyFromArray(data[f, :, :, :].copy().astype(np.float32))
|
||||
else: # 颜色类型
|
||||
grid = vdb.Vec3SGrid()
|
||||
grid.copyFromArray(
|
||||
data[f, :, :, :, :].copy().astype(np.float32))
|
||||
|
||||
# 仅设置transform,不存储metadata
|
||||
grid.transform = vdb.createLinearTransform(
|
||||
layer.affine.transpose())
|
||||
grid.name = layer.kind
|
||||
|
||||
# 保存序列帧VDB
|
||||
data_filepath = cache_path / f"data.{str(f+1).zfill(4)}.vdb"
|
||||
vdb.write(str(data_filepath), grids=[grid])
|
||||
|
||||
else:
|
||||
# 单帧处理
|
||||
if layer.kind in ["label", "scalar"]:
|
||||
grid = vdb.FloatGrid()
|
||||
grid.copyFromArray(data[0, :, :, :].copy().astype(np.float32))
|
||||
else: # 颜色类型
|
||||
grid = vdb.Vec3SGrid()
|
||||
grid.copyFromArray(data[0, :, :, :, :].copy().astype(np.float32))
|
||||
|
||||
# 仅设置transform,不存储metadata
|
||||
grid.transform = vdb.createLinearTransform(layer.affine.transpose())
|
||||
grid.name = layer.kind
|
||||
|
||||
# 保存单帧VDB
|
||||
data_filepath = cache_path / "data.vdb"
|
||||
vdb.write(str(data_filepath), grids=[grid])
|
||||
|
||||
|
||||
def cache_layer_snapshot(layer: Layer, cache_path: str):
|
||||
"""
|
||||
Create and save a low-resolution snapshot (numpy .npy) of the layer and generate PNG slices.
|
||||
|
||||
- Saves a 3D numpy ndarray snapshot to <cache_path>/snapshot.npy.
|
||||
- Then creates per-Z PNGs in the same directory via snapshot_to_pngs.
|
||||
|
||||
Parameters:
|
||||
- layer: Layer to snapshot (expects Layer.snapshot method).
|
||||
- cache_path: destination directory where snapshot.npy and PNGs will be created.
|
||||
"""
|
||||
cache_path = Path(cache_path)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
snapshot_filepath = cache_path / "snapshot.npy"
|
||||
snapshot = layer.snapshot((64, 64, 32))
|
||||
np.save(str(snapshot_filepath), snapshot)
|
||||
snapshot_to_pngs(
|
||||
snapshot, str(cache_path), normalize=layer.kind in ["scalar", "vector"]
|
||||
)
|
||||
|
||||
if layer.kind in ["scalar", "vector", "color"]:
|
||||
plt.figure(figsize=(8, 8))
|
||||
|
||||
def plot_histogram(plt, data, color="black"):
|
||||
flattened = data.flatten()
|
||||
x_min = np.percentile(flattened, 1)
|
||||
x_max = np.percentile(flattened, 99)
|
||||
|
||||
n, bins, _ = plt.hist(
|
||||
flattened,
|
||||
bins=50, # 自动根据数据密度调整bins数量(避免空柱)
|
||||
range=(x_min, x_max), # 只显示核心区域,省略稀疏部分
|
||||
alpha=0,
|
||||
edgecolor="none", # 去除边框,使柱形更紧凑
|
||||
)
|
||||
bin_centers = 0.5 * (bins[1:] + bins[:-1]) # 计算每个bin的中点
|
||||
plt.plot(bin_centers, n, color=color, linewidth=4) # 折线
|
||||
plt.fill_between(bin_centers, n, color=color, alpha=0.1)
|
||||
|
||||
if layer.kind == "scalar":
|
||||
plot_histogram(plt, layer.data[:, :, :, 0], color="black")
|
||||
elif layer.kind in ["vector", "color"]:
|
||||
plot_histogram(plt, layer.data[:, :, :, 0], color="tomato")
|
||||
plot_histogram(plt, layer.data[:, :, :, 1], color="yellowgreen")
|
||||
plot_histogram(plt, layer.data[:, :, :, 2], color="xkcd:azure")
|
||||
|
||||
plt.yscale("log")
|
||||
plt.gca().get_yaxis().set_visible(False)
|
||||
plt.xticks(fontsize=32, rotation=45) # 调整字体大小(数值越大字体越大)
|
||||
|
||||
ax = plt.gca()
|
||||
ax.spines["top"].set_visible(False)
|
||||
ax.spines["right"].set_visible(False)
|
||||
ax.spines["left"].set_visible(False) # 隐藏左边框
|
||||
|
||||
# 调整布局避免标签被截断
|
||||
plt.tight_layout()
|
||||
|
||||
# 保存图片到指定路径
|
||||
save_path = cache_path / "histogram.png"
|
||||
plt.savefig(save_path, bbox_inches="tight", pad_inches=0.1) # 减少边缘留白)
|
||||
|
||||
# 关闭图像释放资源
|
||||
plt.close()
|
||||
|
||||
|
||||
def snapshot_to_pngs(snapshot, cache_path: str, normalize=False):
|
||||
"""
|
||||
Convert a 3D/4D snapshot ndarray into per-slice PNG files.
|
||||
|
||||
- snapshot is expected with axes order X, Y, Z, (C optional).
|
||||
- Writes files snapshot_0.png ... snapshot_{Z-1}.png into cache_path.
|
||||
- Uses ndarray_to_png to perform the per-slice conversion.
|
||||
|
||||
Parameters:
|
||||
- snapshot: numpy ndarray (X, Y, Z[, C])
|
||||
- cache_path: directory where generated PNGs are stored.
|
||||
"""
|
||||
cache_path = Path(cache_path)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if normalize:
|
||||
mn = float(np.percentile(snapshot, 50))
|
||||
mx = float(np.percentile(snapshot, 99))
|
||||
if mx - mn > 1e-8:
|
||||
snapshot = (snapshot - mn) / (mx - mn)
|
||||
else:
|
||||
snapshot = np.clip(snapshot, 0.0, 1.0)
|
||||
|
||||
shape = snapshot.shape
|
||||
for zidx in range(shape[2]):
|
||||
array = snapshot[:, :, zidx, :]
|
||||
ndarray_to_png(array, str(cache_path / f"snapshot_{zidx}.png"))
|
||||
|
||||
|
||||
def get_layer_caches() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Read the saved layer metadata list from Blender's internal text datablock.
|
||||
|
||||
- Looks for a text datablock named by LAYERS_JSON ("bioxel_layers").
|
||||
- Returns the parsed list of layer dictionaries if present; otherwise returns an empty list.
|
||||
|
||||
Returns:
|
||||
- list of layer metadata dictionaries (possibly empty)
|
||||
"""
|
||||
# 检查文本数据块是否存在
|
||||
if LAYERS_JSON not in bpy.data.texts:
|
||||
return []
|
||||
|
||||
# 加载并解析数据
|
||||
layers_text = bpy.data.texts[LAYERS_JSON]
|
||||
try:
|
||||
layers_data = json.loads(layers_text.as_string())
|
||||
# 确保返回格式为列表
|
||||
return layers_data if isinstance(layers_data, list) else []
|
||||
except:
|
||||
return []
|
||||
|
||||
|
||||
def set_layer_caches(layers_data: List[Dict[str, Any]]):
|
||||
"""
|
||||
Write the provided list of layer metadata dictionaries into the Blender text datablock.
|
||||
|
||||
- Ensures the LAYERS_JSON text datablock exists, then overwrites it with pretty JSON.
|
||||
|
||||
Parameters:
|
||||
- layers_data: list of serializable dictionaries describing the layers
|
||||
"""
|
||||
# 获取或创建文本数据块
|
||||
if LAYERS_JSON not in bpy.data.texts:
|
||||
bpy.data.texts.new(LAYERS_JSON)
|
||||
layers_text = bpy.data.texts[LAYERS_JSON]
|
||||
# 写入数据
|
||||
layers_text.clear()
|
||||
layers_text.write(json.dumps(layers_data, indent=4))
|
||||
|
||||
|
||||
def save_layers_to_json(layers: List[Layer], cache_dir: str) -> List[int]:
|
||||
"""
|
||||
Save multiple Layer objects into the internal layers text datablock.
|
||||
|
||||
For each layer:
|
||||
- Generates a unique cache id.
|
||||
- Writes VDB files and a low-resolution snapshot (.npy) plus PNG slices under cache_dir/<cache_id>/.
|
||||
- Appends an entry describing the layer into the b i o x e l _ l a y e r s text datablock.
|
||||
|
||||
Returns:
|
||||
- List of generated cache ids for the saved layers.
|
||||
"""
|
||||
existing_data = get_layer_caches()
|
||||
added_ids = []
|
||||
|
||||
cache_dir_path = Path(cache_dir)
|
||||
cache_dir_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for idx, layer in enumerate(layers):
|
||||
cache_id = str(int(time.time())) + str(idx)
|
||||
cache_path = cache_dir_path / str(cache_id)
|
||||
cache_layer_data(layer, cache_path)
|
||||
cache_layer_snapshot(layer, cache_path)
|
||||
|
||||
# build layer_info
|
||||
cache_info = {
|
||||
"id": cache_id,
|
||||
"name": layer.name,
|
||||
"kind": layer.kind,
|
||||
"shape": layer.shape,
|
||||
"affine": layer.affine.tolist(),
|
||||
"bioxel_size": layer.bioxel_size[0],
|
||||
"dtype": layer.dtype.str,
|
||||
"dtype_num": layer.dtype.num,
|
||||
"frame_count": layer.frame_count,
|
||||
"channel_count": layer.channel_count,
|
||||
"offset": max(0, -layer.min),
|
||||
"min": layer.min,
|
||||
"max": layer.max,
|
||||
"path": bpy.path.abspath(str(cache_path)),
|
||||
"snapshot_z": 0.5,
|
||||
}
|
||||
|
||||
existing_data.append(cache_info)
|
||||
added_ids.append(cache_id)
|
||||
|
||||
set_layer_caches(existing_data)
|
||||
print(
|
||||
f"Successfully added {len(added_ids)} layers to the internal text datablock")
|
||||
return added_ids
|
||||
@@ -1,187 +1,40 @@
|
||||
import bpy
|
||||
from .operators.structure import ExtractMesh
|
||||
|
||||
from .constants import MENU_ITEMS, VERSIONS
|
||||
from .node_menu import NodeMenu
|
||||
|
||||
from .bioxelutils.common import (get_container_obj,
|
||||
get_container_layer_objs, get_layer_label,
|
||||
get_layer_prop_value, get_node_version, is_incompatible)
|
||||
from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer,
|
||||
RemoveSelectedLayers, SaveSelectedLayersCache,
|
||||
ResampleLayer, SignScalar, CombineLabels,
|
||||
FillByLabel, FillByThreshold, FillByRange)
|
||||
from .operators.container import (AddLocator, AddSlicer, ContainerProps,
|
||||
SaveContainerLayersCache, RemoveContainerMissingLayers,
|
||||
SaveContainer, LoadContainer,
|
||||
AddPieCutter, AddPlaneCutter,
|
||||
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
|
||||
ExtractBboxWire, ExtractMesh, ExtractShapeWire,
|
||||
ExtractNodeMesh, ExtractNodeBboxWire, ExtractNodeShapeWire)
|
||||
|
||||
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
|
||||
from .operators.misc import (AddEeveeEnv, CleanTemp, Help,
|
||||
ReLinkNodeLib, RemoveAllMissingLayers,
|
||||
RenderSettingPreset, SaveAllLayersCache,
|
||||
SaveNodeLib, SliceViewer)
|
||||
|
||||
|
||||
class IncompatibleMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_INCOMPATIBLE"
|
||||
bl_label = "Bioxel Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
tip_text = "please downgrade addon version."
|
||||
node_version = get_node_version()
|
||||
if node_version:
|
||||
version_str = "v"+".".join([str(i) for i in list(node_version)])
|
||||
tip_text = f"please downgrade addon version to {version_str}."
|
||||
|
||||
layout = self.layout
|
||||
layout.label(text="Incompatible node version detected.")
|
||||
layout.separator()
|
||||
layout.label(
|
||||
text="If you still want to edit this file with bioxel nodes, ")
|
||||
layout.label(text=tip_text)
|
||||
layout.separator()
|
||||
layout.label(text="If this file is archived, "
|
||||
"please relink node library, ")
|
||||
layout.label(text="check if it still works, "
|
||||
"then save node library.")
|
||||
layout.separator()
|
||||
layout.menu(ReLinkNodeLibMenu.bl_idname)
|
||||
layout.operator(SaveNodeLib.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(DangerZoneMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(RenderSettingMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(Help.bl_idname,
|
||||
icon=Help.bl_icon)
|
||||
|
||||
|
||||
class FetchLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_ADD_LAYER"
|
||||
bl_label = "Fetch Layer"
|
||||
|
||||
def draw(self, context):
|
||||
container_obj = get_container_obj(bpy.context.active_object)
|
||||
layer_objs = get_container_layer_objs(container_obj)
|
||||
layout = self.layout
|
||||
|
||||
for layer_obj in layer_objs:
|
||||
op = layout.operator(FetchLayer.bl_idname,
|
||||
text=get_layer_label(layer_obj))
|
||||
op.layer_obj_name = layer_obj.name
|
||||
|
||||
|
||||
class ExtractFromContainerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_PICK"
|
||||
bl_label = "Extract from Container"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(ExtractMesh.bl_idname)
|
||||
layout.operator(ExtractShapeWire.bl_idname)
|
||||
layout.operator(ExtractBboxWire.bl_idname)
|
||||
|
||||
|
||||
class AddCutterMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
||||
bl_label = "Add a Cutter"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(AddPlaneCutter.bl_idname,
|
||||
icon=AddPlaneCutter.bl_icon, text="Plane Cutter")
|
||||
layout.operator(AddCylinderCutter.bl_idname,
|
||||
icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter")
|
||||
layout.operator(AddCubeCutter.bl_idname,
|
||||
icon=AddCubeCutter.bl_icon, text="Cube Cutter")
|
||||
layout.operator(AddSphereCutter.bl_idname,
|
||||
icon=AddSphereCutter.bl_icon, text="Sphere Cutter")
|
||||
layout.operator(AddPieCutter.bl_idname,
|
||||
icon=AddPieCutter.bl_icon, text="Pie Cutter")
|
||||
from .operators.io import ImportAsLabel, ImportAsScalar, ImportAsColor
|
||||
from .operators.misc import (
|
||||
CleanTemp,
|
||||
Help,
|
||||
RenderSettingPreset,
|
||||
TogglePhantom,
|
||||
)
|
||||
|
||||
|
||||
class ImportLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_IMPORTLAYER"
|
||||
bl_idname = "BIOXEL_MT_IMPORTLAYER"
|
||||
bl_label = "Import Volumetric Data (Init)"
|
||||
bl_icon = "FILE_NEW"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(ImportAsScalar.bl_idname,
|
||||
text="as Scalar")
|
||||
layout.operator(ImportAsLabel.bl_idname,
|
||||
text="as Label")
|
||||
layout.operator(ImportAsColor.bl_idname,
|
||||
text="as Color")
|
||||
|
||||
|
||||
class AddLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_ADDLAYER"
|
||||
bl_label = "Import Volumetric Data (Add)"
|
||||
bl_icon = "FILE_NEW"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(ImportAsScalar.bl_idname,
|
||||
text="as Scalar")
|
||||
layout.operator(ImportAsLabel.bl_idname,
|
||||
text="as Label")
|
||||
layout.operator(ImportAsColor.bl_idname,
|
||||
text="as Color")
|
||||
|
||||
|
||||
class ModifyLayerMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_MODIFYLAYER"
|
||||
bl_label = "Modify Layer"
|
||||
bl_icon = "FILE_NEW"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SignScalar.bl_idname,
|
||||
icon=SignScalar.bl_icon)
|
||||
layout.operator(FillByThreshold.bl_idname,
|
||||
icon=FillByThreshold.bl_icon)
|
||||
layout.operator(FillByRange.bl_idname,
|
||||
icon=FillByRange.bl_icon)
|
||||
layout.operator(FillByLabel.bl_idname,
|
||||
icon=FillByLabel.bl_icon)
|
||||
layout.operator(CombineLabels.bl_idname,
|
||||
icon=CombineLabels.bl_icon)
|
||||
|
||||
|
||||
class ReLinkNodeLibMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_RELINK"
|
||||
bl_label = "Relink Node Library"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
for index, version in enumerate(VERSIONS):
|
||||
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||
text=version["label"])
|
||||
op.index = index
|
||||
layout.operator(ImportAsScalar.bl_idname, text="as Scalar")
|
||||
layout.operator(ImportAsLabel.bl_idname, text="as Label")
|
||||
layout.operator(ImportAsColor.bl_idname, text="as Color")
|
||||
|
||||
|
||||
class RenderSettingMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_RENDER"
|
||||
bl_idname = "BIOXEL_MT_RENDER"
|
||||
bl_label = "Render Setting Presets"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for k, v in RenderSettingPreset.PRESETS.items():
|
||||
op = layout.operator(RenderSettingPreset.bl_idname,
|
||||
text=v)
|
||||
op = layout.operator(RenderSettingPreset.bl_idname, text=v)
|
||||
op.preset = k
|
||||
|
||||
|
||||
class DangerZoneMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_DANGER"
|
||||
bl_idname = "BIOXEL_MT_DANGER"
|
||||
bl_label = "Danger Zone"
|
||||
|
||||
def draw(self, context):
|
||||
@@ -190,118 +43,44 @@ class DangerZoneMenu(bpy.types.Menu):
|
||||
|
||||
|
||||
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_TOPBAR"
|
||||
bl_idname = "BIOXEL_MT_TOPBAR"
|
||||
bl_label = "Bioxel Nodes"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.menu(ImportLayerMenu.bl_idname,
|
||||
icon=ImportLayerMenu.bl_icon)
|
||||
layout.operator(LoadContainer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveAllLayersCache.bl_idname)
|
||||
layout.operator(RemoveAllMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(ReLinkNodeLibMenu.bl_idname)
|
||||
layout.operator(SaveNodeLib.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(DangerZoneMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(AddEeveeEnv.bl_idname)
|
||||
layout.menu(RenderSettingMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(Help.bl_idname,
|
||||
icon=Help.bl_icon)
|
||||
|
||||
|
||||
def TOPBAR(self, context):
|
||||
layout = self.layout
|
||||
|
||||
if is_incompatible():
|
||||
layout.menu(IncompatibleMenu.bl_idname)
|
||||
else:
|
||||
layout.menu(BioxelNodesTopbarMenu.bl_idname)
|
||||
|
||||
|
||||
class NodeHeadMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_HEAD"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
|
||||
layout = self.layout
|
||||
layout.menu(AddLayerMenu.bl_idname,
|
||||
icon=AddLayerMenu.bl_icon)
|
||||
layout.operator(SaveContainer.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ContainerProps.bl_idname)
|
||||
layout.menu(ExtractFromContainerMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveContainerLayersCache.bl_idname,
|
||||
icon=SaveContainerLayersCache.bl_icon)
|
||||
layout.operator(RemoveContainerMissingLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(AddLocator.bl_idname,
|
||||
icon=AddLocator.bl_icon)
|
||||
layout.operator(AddSlicer.bl_idname,
|
||||
icon=AddSlicer.bl_icon)
|
||||
layout.menu(AddCutterMenu.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.menu(FetchLayerMenu.bl_idname)
|
||||
layout.operator(Help.bl_idname, icon=Help.bl_icon)
|
||||
|
||||
|
||||
class NodeContextMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXELNODES_MT_NODE_CONTEXT"
|
||||
bl_idname = "BIOXEL_MT_NODE_CONTEXT"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(ExtractNodeMesh.bl_idname)
|
||||
layout.operator(ExtractNodeShapeWire.bl_idname)
|
||||
layout.operator(ExtractNodeBboxWire.bl_idname)
|
||||
layout.operator(ExtractMesh.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(SaveSelectedLayersCache.bl_idname,
|
||||
icon=SaveSelectedLayersCache.bl_icon)
|
||||
layout.operator(RemoveSelectedLayers.bl_idname)
|
||||
|
||||
layout.separator()
|
||||
layout.operator(RenameLayer.bl_idname,
|
||||
icon=RenameLayer.bl_icon)
|
||||
layout.operator(RetimeLayer.bl_idname)
|
||||
layout.operator(RelocateLayer.bl_idname)
|
||||
class ObjectContextMenu(bpy.types.Menu):
|
||||
bl_idname = "BIOXEL_MT_OBJECT_CONTEXT"
|
||||
bl_label = "Bioxel Nodes"
|
||||
bl_icon = "FILE_VOLUME"
|
||||
|
||||
layout.separator()
|
||||
layout.operator(ResampleLayer.bl_idname,
|
||||
icon=ResampleLayer.bl_icon)
|
||||
layout.operator(SignScalar.bl_idname)
|
||||
layout.operator(FillByThreshold.bl_idname)
|
||||
layout.operator(FillByRange.bl_idname)
|
||||
layout.operator(FillByLabel.bl_idname)
|
||||
layout.operator(CombineLabels.bl_idname)
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(TogglePhantom.bl_idname)
|
||||
|
||||
|
||||
def NODE_CONTEXT(self, context):
|
||||
if is_incompatible():
|
||||
return
|
||||
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
|
||||
if not is_geo_nodes or not is_container:
|
||||
if not is_geo_nodes or not container_obj:
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
@@ -309,127 +88,18 @@ def NODE_CONTEXT(self, context):
|
||||
layout.menu(NodeContextMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_HEAD(self, context):
|
||||
if is_incompatible():
|
||||
return
|
||||
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
|
||||
if not is_geo_nodes or not is_container:
|
||||
return
|
||||
|
||||
# 定义添加到右键菜单的函数,放在首位
|
||||
def OBJECT_CONTEXT(self, context):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(NodeHeadMenu.bl_idname)
|
||||
|
||||
|
||||
def NODE_PROP(self, context):
|
||||
if is_incompatible():
|
||||
return
|
||||
|
||||
container_obj = context.object
|
||||
is_geo_nodes = context.area.ui_type == "GeometryNodeTree"
|
||||
is_container = get_container_obj(container_obj)
|
||||
self.bl_label = "Group"
|
||||
|
||||
if not is_geo_nodes or not is_container:
|
||||
return
|
||||
|
||||
if container_obj.modifiers[0].node_group != context.space_data.edit_tree:
|
||||
return
|
||||
|
||||
self.bl_label = "Bioxel Nodes"
|
||||
|
||||
layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
|
||||
layer_list = layer_list_UL.layer_list
|
||||
layer_list.clear()
|
||||
|
||||
for layer_obj in get_container_layer_objs(container_obj):
|
||||
layer_item = layer_list.add()
|
||||
layer_item.label = get_layer_label(layer_obj)
|
||||
layer_item.obj_name = layer_obj.name
|
||||
layer_item.info_text = "\n".join([f"{prop}: {get_layer_prop_value(layer_obj, prop)}"
|
||||
for prop in ["kind",
|
||||
"bioxel_size",
|
||||
"shape",
|
||||
"frame_count",
|
||||
"channel_count",
|
||||
"min", "max"]])
|
||||
|
||||
layout = self.layout
|
||||
layout.label(text="Layer List")
|
||||
split = layout.row()
|
||||
split.template_list(listtype_name="BIOXELNODES_UL_layer_list",
|
||||
list_id="layer_list",
|
||||
dataptr=layer_list_UL,
|
||||
propname="layer_list",
|
||||
active_dataptr=layer_list_UL,
|
||||
active_propname="layer_list_active",
|
||||
item_dyntip_propname="info_text",
|
||||
rows=20)
|
||||
|
||||
# sidebar = split.column(align=True)
|
||||
# sidebar.menu(AddLayerMenu.bl_idname,
|
||||
# icon=AddLayerMenu.bl_icon, text="")
|
||||
|
||||
# sidebar.operator(SaveContainerLayersCache.bl_idname,
|
||||
# icon=SaveContainerLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveContainerMissingLayers.bl_idname,
|
||||
# icon=RemoveContainerMissingLayers.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SaveSelectedLayersCache.bl_idname,
|
||||
# icon=SaveSelectedLayersCache.bl_icon, text="")
|
||||
# sidebar.operator(RemoveSelectedLayers.bl_idname,
|
||||
# icon=RemoveSelectedLayers.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(RenameLayer.bl_idname,
|
||||
# icon=RenameLayer.bl_icon, text="")
|
||||
# sidebar.operator(ResampleLayer.bl_idname,
|
||||
# icon=ResampleLayer.bl_icon, text="")
|
||||
# sidebar.operator(RetimeLayer.bl_idname,
|
||||
# icon=RetimeLayer.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# sidebar.operator(SignScalar.bl_idname,
|
||||
# icon=SignScalar.bl_icon, text="")
|
||||
# sidebar.operator(FillByThreshold.bl_idname,
|
||||
# icon=FillByThreshold.bl_icon, text="")
|
||||
# sidebar.operator(FillByRange.bl_idname,
|
||||
# icon=FillByRange.bl_icon, text="")
|
||||
# sidebar.operator(FillByLabel.bl_idname,
|
||||
# icon=FillByLabel.bl_icon, text="")
|
||||
|
||||
# sidebar.separator()
|
||||
# layout.separator()
|
||||
|
||||
|
||||
def VIEW3D_TOPBAR(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(SliceViewer.bl_idname)
|
||||
|
||||
|
||||
node_menu = NodeMenu(
|
||||
menu_items=MENU_ITEMS
|
||||
)
|
||||
layout.menu(ObjectContextMenu.bl_idname)
|
||||
|
||||
|
||||
def add():
|
||||
node_menu.register()
|
||||
# bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR)
|
||||
bpy.types.TOPBAR_MT_editor_menus.append(TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.prepend(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.append(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT)
|
||||
bpy.types.VIEW3D_MT_object_context_menu.append(OBJECT_CONTEXT)
|
||||
# bpy.types.NODE_MT_context_menu.append(NODE_CONTEXT)
|
||||
|
||||
|
||||
def remove():
|
||||
# bpy.types.VIEW3D_HT_header.remove(VIEW3D_TOPBAR)
|
||||
bpy.types.TOPBAR_MT_editor_menus.remove(TOPBAR)
|
||||
bpy.types.NODE_PT_node_tree_properties.remove(NODE_PROP)
|
||||
bpy.types.NODE_MT_editor_menus.remove(NODE_HEAD)
|
||||
bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT)
|
||||
node_menu.unregister()
|
||||
bpy.types.VIEW3D_MT_object_context_menu.remove(OBJECT_CONTEXT)
|
||||
# bpy.types.NODE_MT_context_menu.remove(NODE_CONTEXT)
|
||||
|
||||
149
src/bioxelnodes/node.py
Normal file
149
src/bioxelnodes/node.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
# LookupError replaced with built-in LookupError
|
||||
from .constants import LATEST_NODE_LIB_PATH
|
||||
|
||||
|
||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
||||
node.location.x = target_node.location.x + offset[0]
|
||||
node.location.y = target_node.location.y + offset[1]
|
||||
|
||||
|
||||
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
||||
xs = []
|
||||
ys = []
|
||||
for target_node in target_nodes:
|
||||
xs.append(target_node.location.x)
|
||||
ys.append(target_node.location.y)
|
||||
|
||||
node.location.x = sum(xs) / len(xs) + offset[0]
|
||||
node.location.y = sum(ys) / len(ys) + offset[1]
|
||||
|
||||
|
||||
def get_node_type(node):
|
||||
node_type = type(node).__name__
|
||||
if node_type == "GeometryNodeGroup":
|
||||
node_type = node.node_tree.name
|
||||
|
||||
return node_type
|
||||
|
||||
|
||||
def get_nodes_by_type(node_group, type_name: str):
|
||||
return [node for node in node_group.nodes if get_node_type(node) == type_name]
|
||||
|
||||
|
||||
def get_node_tree(node_type: str, use_link=True):
|
||||
try:
|
||||
return bpy.data.node_groups[node_type]
|
||||
except KeyError:
|
||||
lib_path_str = str(LATEST_NODE_LIB_PATH)
|
||||
with bpy.data.libraries.load(lib_path_str, link=use_link, relative=True) as (
|
||||
data_from,
|
||||
data_to,
|
||||
):
|
||||
data_to.node_groups = [
|
||||
n for n in data_from.node_groups if n == node_type]
|
||||
node_tree = data_to.node_groups[0]
|
||||
|
||||
if node_tree is None:
|
||||
raise LookupError("No custom node found")
|
||||
|
||||
return node_tree
|
||||
|
||||
|
||||
def assign_node_tree(node, node_tree):
|
||||
node.node_tree = node_tree
|
||||
node.width = 200.0
|
||||
node.name = node_tree.name
|
||||
return node
|
||||
|
||||
|
||||
def add_node_to_graph(node_name: str, node_group, node_label=None, use_link=True):
|
||||
node_type = f"O {node_name}"
|
||||
node_label = node_label or node_name
|
||||
|
||||
# Deselect all nodes first
|
||||
for node in node_group.nodes:
|
||||
if node.select:
|
||||
node.select = False
|
||||
|
||||
node_tree = get_node_tree(node_type, use_link)
|
||||
node = node_group.nodes.new("GeometryNodeGroup")
|
||||
assign_node_tree(node, node_tree)
|
||||
|
||||
node.label = node_label
|
||||
node.show_options = False
|
||||
bpy.ops.node.view_selected()
|
||||
return node
|
||||
|
||||
|
||||
def get_output_node(node_group):
|
||||
try:
|
||||
output_node = get_nodes_by_type(node_group, "NodeGroupOutput")[0]
|
||||
except:
|
||||
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||
|
||||
return output_node
|
||||
|
||||
|
||||
def add_bioxel_node(name: str):
|
||||
# bpy.ops.node.add_group_asset(
|
||||
# asset_library_type="CUSTOM",
|
||||
# asset_library_identifier = "O Bioxel",
|
||||
# relative_asset_identifier = "NodeTreO Crop Layer"
|
||||
# )
|
||||
# bpy.ops.node.add_group_asset(
|
||||
# asset_library_type="CUSTOM",
|
||||
# asset_library_identifier="O Bioxel",
|
||||
# relative_asset_identifier="Nodes.blend\\NodeTree\\O Render Structure"
|
||||
# )
|
||||
|
||||
try:
|
||||
node_tree = bpy.data.node_groups[name]
|
||||
except KeyError:
|
||||
bpy.ops.object.modifier_add_node_group(
|
||||
asset_library_type="CUSTOM",
|
||||
asset_library_identifier="O Bioxel",
|
||||
relative_asset_identifier=f"Nodes.blend\\NodeTree\\{name}",
|
||||
)
|
||||
bpy.ops.object.modifier_remove(modifier=name)
|
||||
try:
|
||||
node_tree = bpy.data.node_groups[name]
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
bpy.ops.node.add_node(
|
||||
"INVOKE_DEFAULT", type="GeometryNodeGroup", use_transform=True
|
||||
)
|
||||
|
||||
node = bpy.context.active_node
|
||||
node.node_tree = node_tree
|
||||
node.show_options = False
|
||||
|
||||
return bpy.context.active_node
|
||||
|
||||
|
||||
def get_layer_nodes(node_group):
|
||||
"""Return all O Layer nodes in the given node_tree."""
|
||||
return [
|
||||
n
|
||||
for n in node_group.nodes
|
||||
if n.bl_idname == "GeometryNodeGroup"
|
||||
and getattr(n, "node_tree", None)
|
||||
and n.node_tree.name.startswith("O Layer")
|
||||
]
|
||||
|
||||
|
||||
def get_main_node_group(context):
|
||||
space = getattr(context, "space_data", None) or getattr(
|
||||
context, "space", None)
|
||||
if not space:
|
||||
return None
|
||||
node_tree = getattr(space, "node_tree", None)
|
||||
if not node_tree:
|
||||
return None
|
||||
# accept GeometryNodeTree by bl_idname or class name for compatibility
|
||||
if getattr(node_tree, "bl_idname", "") == "GeometryNodeTree" or node_tree.__class__.__name__ == "GeometryNodeTree":
|
||||
return node_tree
|
||||
return None
|
||||
@@ -1,87 +0,0 @@
|
||||
import bpy
|
||||
from .operators.node import AddNode
|
||||
|
||||
|
||||
class NodeMenu():
|
||||
def __init__(
|
||||
self,
|
||||
menu_items,
|
||||
) -> None:
|
||||
|
||||
self.menu_items = menu_items
|
||||
self.class_prefix = f"BIOXELNODES_MT"
|
||||
root_label = "Bioxel Nodes"
|
||||
menu_classes = []
|
||||
self._create_menu_class(
|
||||
items=menu_items,
|
||||
label=root_label,
|
||||
menu_classes=menu_classes
|
||||
)
|
||||
self.menu_classes = menu_classes
|
||||
|
||||
idname = f"{self.class_prefix}_{root_label.replace(' ', '').upper()}"
|
||||
icon = "FILE_VOLUME"
|
||||
|
||||
def drew_menu(self, context):
|
||||
if (bpy.context.area.spaces[0].tree_type == 'GeometryNodeTree'):
|
||||
layout = self.layout
|
||||
layout.separator()
|
||||
layout.menu(idname,
|
||||
icon=icon)
|
||||
self.drew_menu = drew_menu
|
||||
|
||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
||||
idname_namespace = idname_namespace or self.class_prefix
|
||||
idname = f"{idname_namespace}_{label.replace(' ', '').upper()}"
|
||||
|
||||
# create submenu class if item is menu.
|
||||
for item in items:
|
||||
item_items = item.get('items') if item != 'separator' else None
|
||||
if item_items:
|
||||
menu_class = self._create_menu_class(
|
||||
menu_classes=menu_classes,
|
||||
items=item_items,
|
||||
label=item.get('label') or 'CustomNodes',
|
||||
icon=item.get('icon') or 0,
|
||||
idname_namespace=idname
|
||||
)
|
||||
item['menu_class'] = menu_class
|
||||
|
||||
# create menu class
|
||||
class Menu(bpy.types.Menu):
|
||||
bl_idname = idname
|
||||
bl_label = label
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
for item in items:
|
||||
if item == "separator":
|
||||
layout.separator()
|
||||
elif item.get('menu_class'):
|
||||
layout.menu(
|
||||
item.get('menu_class').bl_idname,
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
else:
|
||||
op = layout.operator(
|
||||
AddNode.bl_idname,
|
||||
text=item.get('label'),
|
||||
icon=item.get('icon') or 'NONE'
|
||||
)
|
||||
op.node_name = item['name']
|
||||
op.node_label = item.get('label') or ""
|
||||
op.node_description = item.get(
|
||||
'node_description') or ""
|
||||
|
||||
menu_classes.append(Menu)
|
||||
return Menu
|
||||
|
||||
def register(self):
|
||||
for cls in self.menu_classes:
|
||||
bpy.utils.register_class(cls)
|
||||
bpy.types.NODE_MT_add.append(self.drew_menu)
|
||||
|
||||
def unregister(self):
|
||||
bpy.types.NODE_MT_add.remove(self.drew_menu)
|
||||
for cls in reversed(self.menu_classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
@@ -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"}
|
||||
180
src/bioxelnodes/panels.py
Normal file
180
src/bioxelnodes/panels.py
Normal 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")
|
||||
@@ -8,22 +8,10 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||
cache_dir: bpy.props.StringProperty(
|
||||
name="Set Cache Directory",
|
||||
subtype='DIR_PATH',
|
||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||
default=str(Path(Path.home(), '.bioxel'))
|
||||
) # type: ignore
|
||||
|
||||
change_render_setting: bpy.props.BoolProperty(
|
||||
name="Change Render Setting on First Import",
|
||||
default=True,
|
||||
) # type: ignore
|
||||
|
||||
node_import_method: bpy.props.EnumProperty(name="Node Import Method",
|
||||
default="LINK",
|
||||
items=[("LINK", "Link", ""),
|
||||
("APPEND", "Append (Reuse Data)", "")]) # type: ignore
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="Configuration")
|
||||
layout.prop(self, 'cache_dir')
|
||||
layout.prop(self, "change_render_setting")
|
||||
layout.prop(self, "node_import_method")
|
||||
|
||||
@@ -1,44 +1,134 @@
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
|
||||
from .bioxelutils.common import get_node_type
|
||||
from .operators.layer import SelectAndFocusNode
|
||||
from .utils import load_icon
|
||||
from .layer import get_layer_caches, set_layer_caches
|
||||
|
||||
|
||||
class BIOXELNODES_UL_layer_list(bpy.types.UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
layout.label(text=item.label, translate=False, icon_value=icon)
|
||||
def get_snapshot_icon(cache, z: float):
|
||||
cache_id = str(cache["id"])
|
||||
shape = (64, 64, 32)
|
||||
zidx = int(z * (shape[2] - 1))
|
||||
|
||||
path = Path(bpy.path.abspath(cache["path"]))
|
||||
return load_icon(path / f"snapshot_{zidx}.png", f"{cache_id}_{zidx}")
|
||||
|
||||
|
||||
class BIOXELNODES_Series(bpy.types.PropertyGroup):
|
||||
id: bpy.props.StringProperty() # type: ignore
|
||||
label: bpy.props.StringProperty() # type: ignore
|
||||
def _bioxel_layer_items(self, context):
|
||||
"""
|
||||
EnumProperty items callback: 从 get_layer_caches 构造枚举项。
|
||||
使用 PREVIEWS 加载每个 layer 的 thumbnail_png(如果存在)并返回 icon_id。
|
||||
"""
|
||||
items = []
|
||||
caches = get_layer_caches()
|
||||
|
||||
for idx, cache in enumerate(caches):
|
||||
cache_id = str(cache["id"])
|
||||
name = cache.get("name", f"Layer ?")
|
||||
description = cache.get("kind", "?")
|
||||
z = cache.get("snapshot_z", 0.5)
|
||||
icon = get_snapshot_icon(cache, z)
|
||||
# 在这个context下,只能加载无法生成。
|
||||
items.append((cache_id, name, description, icon, idx))
|
||||
|
||||
if not items:
|
||||
items = [("nothing_found", "No Layers",
|
||||
"No layers found", "QUESTION", 0)]
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def select_layer(self, context):
|
||||
layer_list_UL = bpy.context.window_manager.bioxelnodes_layer_list_UL
|
||||
layer_list = layer_list_UL.layer_list
|
||||
layer_list_active = layer_list_UL.layer_list_active
|
||||
|
||||
if len(layer_list) > 0 and layer_list_active != -1 and layer_list_active < len(layer_list):
|
||||
layer_obj = bpy.data.objects[layer_list[layer_list_active].obj_name]
|
||||
node_group = context.space_data.edit_tree
|
||||
for node in node_group.nodes:
|
||||
node.select = False
|
||||
if get_node_type(node) == "BioxelNodes_FetchLayer":
|
||||
if node.inputs[0].default_value == layer_obj:
|
||||
node.select = True
|
||||
|
||||
layer_list_UL.layer_list_active = -1
|
||||
def _update_layer_gallery(self, context):
|
||||
wm = context.window_manager
|
||||
selected_id = getattr(wm, "bioxel_layer_library", None)
|
||||
cache = next(
|
||||
(c for c in get_layer_caches() if str(c.get("id", "")) == str(selected_id)),
|
||||
None,
|
||||
)
|
||||
z = cache.get("snapshot_z", 0.5) if cache else 0.5
|
||||
setattr(wm, "bioxel_snapshot_z", z)
|
||||
|
||||
|
||||
class BIOXELNODES_Layer(bpy.types.PropertyGroup):
|
||||
obj_name: bpy.props.StringProperty() # type: ignore
|
||||
label: bpy.props.StringProperty() # type: ignore
|
||||
info_text: bpy.props.StringProperty() # type: ignore
|
||||
def _update_snapshot_z(self, context):
|
||||
"""
|
||||
update callback for WindowManager.bioxel_thumb_z
|
||||
- write a slice PNG from the saved .npy
|
||||
- update PREVIEWS entry for the selected layer
|
||||
- force template_icon_view to refresh by briefly cycling the EnumProperty value
|
||||
- force UI redraw
|
||||
"""
|
||||
wm = context.window_manager
|
||||
selected_id = getattr(wm, "bioxel_layer_library", None)
|
||||
|
||||
if not selected_id or selected_id == "nothing_found":
|
||||
return
|
||||
|
||||
try:
|
||||
selected_id = int(selected_id)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
caches = get_layer_caches()
|
||||
z = getattr(wm, "bioxel_snapshot_z", 0.5)
|
||||
|
||||
for cache in caches:
|
||||
if int(cache.get("id", -1)) == selected_id:
|
||||
cache["snapshot_z"] = z
|
||||
break
|
||||
|
||||
set_layer_caches(caches)
|
||||
|
||||
|
||||
class BIOXELNODES_LayerListUL(bpy.types.PropertyGroup):
|
||||
layer_list: bpy.props.CollectionProperty(
|
||||
type=BIOXELNODES_Layer) # type: ignore
|
||||
layer_list_active: bpy.props.IntProperty(default=-1,
|
||||
update=select_layer,
|
||||
options=set()) # type: ignore
|
||||
class BIOXEL_Series(bpy.types.PropertyGroup):
|
||||
id: bpy.props.StringProperty() # type: ignore
|
||||
label: bpy.props.StringProperty() # type: ignore
|
||||
|
||||
|
||||
class BIOXEL_UL_layer_nodes(bpy.types.UIList):
|
||||
"""UIList for O Layer nodes only"""
|
||||
|
||||
bl_idname = "BIOXEL_UL_layer_nodes"
|
||||
|
||||
def filter_items(self, context, data, propname):
|
||||
nodes = getattr(data, propname)
|
||||
flt_flags = []
|
||||
flt_neworder = []
|
||||
|
||||
for node in nodes:
|
||||
# 只保留 O Layer 节点
|
||||
if (
|
||||
hasattr(node, "bl_idname")
|
||||
and node.bl_idname.startswith("GeometryNodeGroup")
|
||||
and getattr(getattr(node, "node_tree", None), "name", "").startswith(
|
||||
"O Layer"
|
||||
)
|
||||
):
|
||||
flt_flags.append(self.bitflag_filter_item)
|
||||
else:
|
||||
flt_flags.append(0)
|
||||
return flt_flags, flt_neworder
|
||||
|
||||
def draw_item(
|
||||
self, context, layout, data, item, icon, active_data, active_propname, index
|
||||
):
|
||||
node = item
|
||||
# 这里只画 O Layer 节点,其他已被 filter_items 过滤
|
||||
split = layout.split(factor=0.3, align=True)
|
||||
col1 = split.row(align=True)
|
||||
col2 = split.row(align=True)
|
||||
name_socket = node.inputs["Name"]
|
||||
resample_socket = node.inputs["Resample"]
|
||||
scale_socket = node.inputs["Bioxel Scale"]
|
||||
name = str(node.inputs["Name"].default_value)
|
||||
|
||||
# row.prop(name_socket, "default_value", text="")
|
||||
col1.prop(resample_socket, "default_value",
|
||||
icon="UV_DATA", text="", toggle=1)
|
||||
col1.prop(scale_socket, "default_value", text="")
|
||||
col2.label(text=name)
|
||||
|
||||
op = col2.operator(
|
||||
SelectAndFocusNode.bl_idname, text="", icon="RESTRICT_SELECT_OFF", emboss=True
|
||||
)
|
||||
op.node_name = node.name
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
|
||||
import shutil
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
from .node import get_nodes_by_type
|
||||
from .constants import LATEST_NODE_LIB_PATH, NODE_LIB_DIRPATH, PREVIEW_COLLECTIONS
|
||||
|
||||
|
||||
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||
@@ -44,16 +50,16 @@ def select_object(target_obj):
|
||||
def progress_bar(self, context):
|
||||
row = self.layout.row()
|
||||
row.progress(
|
||||
factor=context.window_manager.bioxelnodes_progress_factor,
|
||||
factor=context.window_manager.bioxel_progress_factor,
|
||||
type="BAR",
|
||||
text=context.window_manager.bioxelnodes_progress_text
|
||||
text=context.window_manager.bioxel_progress_text
|
||||
)
|
||||
row.scale_x = 2
|
||||
|
||||
|
||||
def progress_update(context, factor, text=""):
|
||||
context.window_manager.bioxelnodes_progress_factor = factor
|
||||
context.window_manager.bioxelnodes_progress_text = text
|
||||
bpy.context.window_manager.bioxel_progress_factor = factor
|
||||
bpy.context.window_manager.bioxel_progress_text = text
|
||||
|
||||
|
||||
def get_preferences():
|
||||
@@ -62,11 +68,335 @@ def get_preferences():
|
||||
|
||||
def get_cache_dir():
|
||||
preferences = get_preferences()
|
||||
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||
cache_path = Path(preferences.cache_dir)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
return str(cache_path)
|
||||
return cache_path
|
||||
|
||||
|
||||
def get_use_link():
|
||||
preferences = get_preferences()
|
||||
return preferences.node_import_method == "LINK"
|
||||
|
||||
|
||||
def get_container_objs_from_selection():
|
||||
container_objs = []
|
||||
for obj in bpy.context.selected_objects:
|
||||
if get_container_obj(obj):
|
||||
container_objs.append(obj)
|
||||
|
||||
return list(set(container_objs))
|
||||
|
||||
|
||||
def get_container_obj(current_obj):
|
||||
if current_obj:
|
||||
if current_obj.get('bioxel_container'):
|
||||
return current_obj
|
||||
elif current_obj.get('bioxel_layer'):
|
||||
parent = current_obj.parent
|
||||
return parent if parent.get('bioxel_container') else None
|
||||
return None
|
||||
|
||||
|
||||
def get_layer_prop_value(layer_obj: bpy.types.Object, prop_name: str):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
prop = layer_node.inputs.get(prop_name)
|
||||
if prop is None:
|
||||
return None
|
||||
|
||||
value = prop.default_value
|
||||
|
||||
if type(value).__name__ == "bpy_prop_array":
|
||||
value = tuple(value)
|
||||
return tuple([int(v) for v in value]) \
|
||||
if prop in ["shape"] else value
|
||||
elif type(value).__name__ == "str":
|
||||
return str(value)
|
||||
elif type(value).__name__ == "float":
|
||||
value = float(value)
|
||||
return round(value, 2) \
|
||||
if prop in ["bioxel_size"] else value
|
||||
elif type(value).__name__ == "int":
|
||||
value = int(value)
|
||||
return value
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def get_layer_name(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "name")
|
||||
|
||||
|
||||
def get_layer_kind(layer_obj):
|
||||
return get_layer_prop_value(layer_obj, "kind")
|
||||
|
||||
|
||||
def get_layer_label(layer_obj):
|
||||
name = get_layer_name(layer_obj)
|
||||
# kind = get_layer_kind(layer_obj)
|
||||
|
||||
label = f"{name}"
|
||||
|
||||
if is_missing_layer(layer_obj):
|
||||
return "**MISSING**" + label
|
||||
elif is_temp_layer(layer_obj):
|
||||
return "* " + label
|
||||
else:
|
||||
return label
|
||||
|
||||
|
||||
def is_missing_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
return not cache_filepath.is_file()
|
||||
|
||||
|
||||
def is_temp_layer(layer_obj):
|
||||
cache_filepath = Path(bpy.path.abspath(
|
||||
layer_obj.data.filepath)).resolve()
|
||||
cache_dirpath = Path(get_cache_dir())
|
||||
return cache_dirpath in cache_filepath.parents
|
||||
|
||||
|
||||
def set_layer_prop_value(layer_obj: bpy.types.Object, prop: str, value):
|
||||
node_group = layer_obj.modifiers[0].node_group
|
||||
layer_node = get_nodes_by_type(node_group, "BioxelNodes__Layer")[0]
|
||||
layer_node.inputs[prop].default_value = value
|
||||
|
||||
|
||||
def get_layer_obj(current_obj: bpy.types.Object):
|
||||
if current_obj:
|
||||
if current_obj.get('bioxel_layer') and current_obj.parent:
|
||||
if current_obj.parent.get('bioxel_container'):
|
||||
return current_obj
|
||||
return None
|
||||
|
||||
|
||||
def get_container_layer_objs(container_obj: bpy.types.Object):
|
||||
layer_objs = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.parent == container_obj and get_layer_obj(obj):
|
||||
layer_objs.append(obj)
|
||||
|
||||
return layer_objs
|
||||
|
||||
|
||||
def get_all_layer_objs():
|
||||
layer_objs = []
|
||||
for obj in bpy.data.objects:
|
||||
if get_layer_obj(obj):
|
||||
layer_objs.append(obj)
|
||||
|
||||
return layer_objs
|
||||
|
||||
|
||||
def add_driver(target, target_prop, var_sources, expression):
|
||||
driver = target.driver_add(target_prop)
|
||||
is_vector = isinstance(driver, list)
|
||||
drivers = driver if is_vector else [driver]
|
||||
|
||||
for i, driver in enumerate(drivers):
|
||||
for j, var_source in enumerate(var_sources):
|
||||
|
||||
source = var_source['source']
|
||||
prop = var_source['prop']
|
||||
|
||||
var = driver.driver.variables.new()
|
||||
var.name = f"var{j}"
|
||||
|
||||
var.targets[0].id_type = source.id_type
|
||||
var.targets[0].id = source
|
||||
var.targets[0].data_path = f'["{prop}"][{i}]'\
|
||||
if is_vector else f'["{prop}"]'
|
||||
|
||||
driver.driver.expression = expression
|
||||
|
||||
|
||||
def add_direct_driver(target, target_prop, source, source_prop):
|
||||
drivers = [
|
||||
{
|
||||
"source": source,
|
||||
"prop": source_prop
|
||||
}
|
||||
]
|
||||
expression = "var0"
|
||||
add_driver(target, target_prop, drivers, expression)
|
||||
|
||||
|
||||
def read_file_prop(content: str):
|
||||
props = {}
|
||||
for line in content.split("\n"):
|
||||
line = line.replace(" ", "")
|
||||
p = line.split("=")[0]
|
||||
if p != "":
|
||||
v = line.split("=")[-1]
|
||||
props[p] = v
|
||||
return props
|
||||
|
||||
|
||||
def write_file_prop(props: dict):
|
||||
lines = []
|
||||
for p, v in props.items():
|
||||
lines.append(f"{p} = {v}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def set_file_prop(prop, value):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
props[prop] = value
|
||||
bpy.data.texts["BioxelNodes"].clear()
|
||||
bpy.data.texts["BioxelNodes"].write(write_file_prop(props))
|
||||
|
||||
|
||||
def get_file_prop(prop):
|
||||
if bpy.data.texts.get("BioxelNodes") is None:
|
||||
bpy.data.texts.new("BioxelNodes")
|
||||
|
||||
props = read_file_prop(bpy.data.texts["BioxelNodes"].as_string())
|
||||
return props.get(prop)
|
||||
|
||||
|
||||
def get_node_version():
|
||||
node_version = get_file_prop("node_version")
|
||||
return literal_eval(node_version) if node_version else None
|
||||
|
||||
|
||||
def is_incompatible():
|
||||
return False
|
||||
|
||||
|
||||
def get_node_lib_path(node_version):
|
||||
version_str = "v"+".".join([str(i) for i in list(node_version)])
|
||||
lib_filename = f"BioxelNodes_{version_str}.blend"
|
||||
return Path(NODE_LIB_DIRPATH,
|
||||
lib_filename).resolve()
|
||||
|
||||
|
||||
def local_lib_not_updated():
|
||||
return False
|
||||
|
||||
|
||||
def ndarray_to_png(array: np.ndarray, save_path: str):
|
||||
"""
|
||||
Create a PNG from a 2D or 3-channel slice array using Blender API.
|
||||
slice_arr expected shape: (W, H) or (W, H, 1) or (W,H,3). Values assumed numeric.
|
||||
|
||||
The function:
|
||||
- Converts single-channel arrays to RGB by repeating channel.
|
||||
- Flips the image vertically to match Blender's origin.
|
||||
- Writes the image file to save_path using a temporary Blender image datablock.
|
||||
|
||||
Parameters:
|
||||
- array: numpy ndarray representing the slice (W,H,C) or (W,H,1) or (W,H)
|
||||
- save_path: destination PNG filepath
|
||||
"""
|
||||
save_path = Path(save_path)
|
||||
h, w = array.shape[0], array.shape[1]
|
||||
if array.shape[2] == 1:
|
||||
img = np.concatenate([array, array, array], axis=2)
|
||||
else:
|
||||
img = array[..., :3]
|
||||
|
||||
# Blender expects pixels as sequence of floats in RGBA, length w*h*4
|
||||
# NOTE: Blender's image origin is bottom-left; we'll flip vertically for visual consistency.
|
||||
img = np.flipud(img)
|
||||
pixels = np.empty((h * w * 4,), dtype=np.float32)
|
||||
# fill
|
||||
pixels[0::4] = img[..., 0].ravel()
|
||||
pixels[1::4] = img[..., 1].ravel()
|
||||
pixels[2::4] = img[..., 2].ravel()
|
||||
pixels[3::4] = 1.0
|
||||
|
||||
# create a temporary image datablock, save then remove
|
||||
name = "tmp"
|
||||
image = bpy.data.images.new(
|
||||
name, width=w, height=h, alpha=True, float_buffer=False)
|
||||
image.pixels = pixels.tolist()
|
||||
# ensure format and filepath then save
|
||||
image.filepath_raw = str(save_path)
|
||||
image.file_format = "PNG"
|
||||
image.save()
|
||||
# remove from data to avoid clutter
|
||||
try:
|
||||
bpy.data.images.remove(image)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def split_text_to_lines(text: str, max_chars: int = 60) -> list:
|
||||
"""
|
||||
Split `text` into multiple lines not exceeding `max_chars` where possible.
|
||||
Breaks on whitespace; long words are chunked.
|
||||
Returns list of line strings.
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
words = text.split()
|
||||
lines = []
|
||||
cur = ""
|
||||
for w in words:
|
||||
if cur:
|
||||
candidate = cur + " " + w
|
||||
else:
|
||||
candidate = w
|
||||
if len(candidate) <= max_chars:
|
||||
cur = candidate
|
||||
else:
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
# if single word longer than max_chars, chunk it
|
||||
if len(w) > max_chars:
|
||||
for i in range(0, len(w), max_chars):
|
||||
lines.append(w[i: i + max_chars])
|
||||
cur = ""
|
||||
else:
|
||||
cur = w
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
return lines
|
||||
|
||||
|
||||
def wrapped_label(layout, text: str, max_chars: int = 42):
|
||||
"""
|
||||
Helper to add multiple layout.label calls for a long text.
|
||||
"""
|
||||
for line in split_text_to_lines(text, max_chars=max_chars):
|
||||
layout.label(text=line)
|
||||
|
||||
|
||||
def refresh_bioxel_panels(context):
|
||||
"""
|
||||
Force Bioxel UI to refresh.
|
||||
|
||||
- Tags relevant areas for redraw.
|
||||
- Nudges the bioxel_layer_gallery EnumProperty to force items() re-evaluation
|
||||
when possible (helps template_icon_view pick up updated icons/paths).
|
||||
Use this from operators after they change b i o x e l _ l a y e r s or previews.
|
||||
"""
|
||||
|
||||
# tag redraw for all windows/areas
|
||||
for window in context.window_manager.windows:
|
||||
for area in window.screen.areas:
|
||||
# target node editor and other UI areas that may show previews
|
||||
if area.type in {"NODE_EDITOR", "VIEW_3D", "IMAGE_EDITOR", "OUTLINER"}:
|
||||
area.tag_redraw()
|
||||
|
||||
|
||||
def load_icon(filepath: Path, key: str):
|
||||
pcoll = PREVIEW_COLLECTIONS["layers"]
|
||||
|
||||
try:
|
||||
icon = pcoll[key].icon_id
|
||||
except KeyError:
|
||||
if filepath.exists():
|
||||
pcoll.load(key, str(filepath), "IMAGE")
|
||||
|
||||
try:
|
||||
icon = pcoll[key].icon_id
|
||||
except KeyError:
|
||||
icon = "TEXTURE"
|
||||
|
||||
return icon
|
||||
|
||||
Reference in New Issue
Block a user