Compare commits
15 Commits
v0.2.9
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c71417e5d | ||
| 655b19bc7c | |||
| 051df670a5 | |||
| 9e885865f7 | |||
|
|
21e0b85662 | ||
| 38fb888f00 | |||
|
|
2429eef5e0 | ||
| 2563f398da | |||
| 0cad35e2da | |||
| 65edf40164 | |||
| c3f88b7846 | |||
|
|
088d9c1edb | ||
| a6ec769a9b | |||
|
|
7430fbf3ef | ||
| ce397b35aa |
@@ -50,33 +50,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
upload_blender_addon:
|
build_blender_add-on:
|
||||||
name: Upload Blender Add-on
|
name: Build Blender Extension
|
||||||
needs: draft_release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
lfs: "true"
|
|
||||||
- name: Zip Add-on
|
|
||||||
run: |
|
|
||||||
zip -r package.zip bioxelnodes
|
|
||||||
- name: Upload Release Asset
|
|
||||||
id: upload-release-asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
|
||||||
asset_path: ./package.zip
|
|
||||||
asset_name: BioxelNodes_Addon_${{ needs.draft_release.outputs.version }}.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
upload_blender_extension:
|
|
||||||
name: Upload Blender Extension
|
|
||||||
needs: draft_release
|
needs: draft_release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: ["windows-x64", "linux-x64", "macos-arm64", "macos-x64"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -86,21 +66,10 @@ jobs:
|
|||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.11
|
||||||
- name: Zip Extension
|
- name: Build Add-on
|
||||||
run: |
|
run: |
|
||||||
pip download SimpleITK==2.3.1 --dest wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
|
pip install tomlkit
|
||||||
pip download pyometiff==1.0.0 --dest wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
|
python build.py ${{ matrix.platform }}
|
||||||
pip download mrcfile==1.5.1 --dest wheels --only-binary=:all: --python-version=3.11 --platform=win_amd64
|
|
||||||
mkdir bioxelnodes/wheels
|
|
||||||
cp wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl bioxelnodes/wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl
|
|
||||||
cp wheels/lxml-5.2.2-cp311-cp311-win_amd64.whl bioxelnodes/wheels/lxml-5.2.2-cp311-cp311-win_amd64.whl
|
|
||||||
cp wheels/tifffile-2024.7.21-py3-none-any.whl bioxelnodes/wheels/tifffile-2024.7.21-py3-none-any.whl
|
|
||||||
cp wheels/pyometiff-1.0.0-py3-none-any.whl bioxelnodes/wheels/pyometiff-1.0.0-py3-none-any.whl
|
|
||||||
cp wheels/mrcfile-1.5.1-py2.py3-none-any.whl bioxelnodes/wheels/mrcfile-1.5.1-py2.py3-none-any.whl
|
|
||||||
rm -r bioxelnodes/externalpackage
|
|
||||||
cp extension/__init__.py bioxelnodes/__init__.py
|
|
||||||
cp extension/preferences.py bioxelnodes/preferences.py
|
|
||||||
cp extension/blender_manifest.toml bioxelnodes/blender_manifest.toml
|
|
||||||
zip -r package.zip bioxelnodes
|
zip -r package.zip bioxelnodes
|
||||||
- name: Upload Release Asset
|
- name: Upload Release Asset
|
||||||
id: upload-release-asset
|
id: upload-release-asset
|
||||||
@@ -110,5 +79,5 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
upload_url: ${{ needs.draft_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
|
||||||
asset_path: ./package.zip
|
asset_path: ./package.zip
|
||||||
asset_name: BioxelNodes_Extension_${{ needs.draft_release.outputs.version }}.zip
|
asset_name: BioxelNodes_${{ needs.draft_release.outputs.version }}_${{ matrix.platform }}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -133,4 +133,6 @@ blendcache_*
|
|||||||
|
|
||||||
.secrets
|
.secrets
|
||||||
|
|
||||||
!bioxelnodes/scipy/_nd_image.cp311-win_amd64.pyd
|
.vdb
|
||||||
|
|
||||||
|
!scipy_ndimage/*/**
|
||||||
@@ -13,7 +13,7 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Fantastic rendering result, also support EEVEE NEXT.
|
- Realistic rendering result, also support EEVEE NEXT.
|
||||||
- Support multiple formats.
|
- Support multiple formats.
|
||||||
- Support 4D volumetric data.
|
- Support 4D volumetric data.
|
||||||
- All kinds of cutters.
|
- All kinds of cutters.
|
||||||
@@ -61,13 +61,13 @@ Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any
|
|||||||
|
|
||||||
## Compatible to Newer Version
|
## Compatible to Newer Version
|
||||||
|
|
||||||
**Updating this addon may break old files, so read the following carefully before updating**
|
**v0.3.x is not compatible to v0.2.x, Updating this addon may break old files. Read the following carefully before upgradation**
|
||||||
|
|
||||||
Before updating this addon, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
Before upgradation, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
||||||
|
|
||||||
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
||||||
|
|
||||||
Also, unlike the newer versions, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
Also, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from .props import BIOXELNODES_LayerListUL
|
||||||
from . import auto_load
|
from . import auto_load
|
||||||
from . import menus
|
from . import menus
|
||||||
|
|
||||||
|
|
||||||
bl_info = {
|
|
||||||
"name": "Bioxel Nodes",
|
|
||||||
"author": "Ma Nan",
|
|
||||||
"description": "",
|
|
||||||
"blender": (4, 1, 0),
|
|
||||||
"version": (0, 2, 9),
|
|
||||||
"location": "File -> Import",
|
|
||||||
"warning": "",
|
|
||||||
"category": "Node"
|
|
||||||
}
|
|
||||||
|
|
||||||
auto_load.init()
|
auto_load.init()
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
auto_load.register()
|
auto_load.register()
|
||||||
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty()
|
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_progress_text = bpy.props.StringProperty()
|
||||||
|
bpy.types.WindowManager.bioxelnodes_layer_list_UL = bpy.props.PointerProperty(
|
||||||
|
type=BIOXELNODES_LayerListUL)
|
||||||
menus.add()
|
menus.add()
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
menus.remove()
|
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()
|
auto_load.unregister()
|
||||||
|
|||||||
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.1.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.3.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v0.3.x.blend
LFS
Normal file
Binary file not shown.
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
BIN
bioxelnodes/assets/Nodes/BioxelNodes_v1.0.x.blend
LFS
Normal file
Binary file not shown.
0
bioxelnodes/bioxel/__init__.py
Normal file
0
bioxelnodes/bioxel/__init__.py
Normal file
9
bioxelnodes/bioxel/container.py
Normal file
9
bioxelnodes/bioxel/container.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from .layer import Layer
|
||||||
|
|
||||||
|
|
||||||
|
class Container():
|
||||||
|
def __init__(self,
|
||||||
|
name,
|
||||||
|
layers: list[Layer] = []) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.layers = layers
|
||||||
43
bioxelnodes/bioxel/io.py
Normal file
43
bioxelnodes/bioxel/io.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import h5py
|
||||||
|
|
||||||
|
from .container import Container
|
||||||
|
from .layer import Layer
|
||||||
|
|
||||||
|
|
||||||
|
def load_container(load_file: str):
|
||||||
|
load_path = Path(load_file).resolve()
|
||||||
|
with h5py.File(load_path, 'r') as file:
|
||||||
|
layers = []
|
||||||
|
for key, layer_dset in file['layers'].items():
|
||||||
|
layers.append(Layer(data=layer_dset[:],
|
||||||
|
name=layer_dset.attrs['name'],
|
||||||
|
kind=layer_dset.attrs['kind'],
|
||||||
|
affine=layer_dset.attrs['affine']))
|
||||||
|
|
||||||
|
container = Container(name=file.attrs['name'],
|
||||||
|
layers=layers)
|
||||||
|
|
||||||
|
return container
|
||||||
|
|
||||||
|
|
||||||
|
def save_container(container: Container, save_file: str, overwrite=False):
|
||||||
|
save_path = Path(save_file).resolve()
|
||||||
|
if overwrite:
|
||||||
|
if save_path.is_file():
|
||||||
|
save_path.unlink()
|
||||||
|
|
||||||
|
with h5py.File(save_path, "w") as file:
|
||||||
|
file.attrs['name'] = container.name
|
||||||
|
layer_group = file.create_group("layers")
|
||||||
|
for layer in container.layers:
|
||||||
|
layer_key = uuid.uuid4().hex[:8]
|
||||||
|
layer_dset = layer_group.create_dataset(name=layer_key,
|
||||||
|
data=layer.data)
|
||||||
|
layer_dset.attrs['name'] = layer.name
|
||||||
|
layer_dset.attrs['kind'] = layer.kind
|
||||||
|
layer_dset.attrs['affine'] = layer.affine
|
||||||
156
bioxelnodes/bioxel/layer.py
Normal file
156
bioxelnodes/bioxel/layer.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import copy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from . import scipy
|
||||||
|
from . import scipy as ndi
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import transforms3d
|
||||||
|
# TODO: turn to dataclasses
|
||||||
|
|
||||||
|
|
||||||
|
class Layer():
|
||||||
|
def __init__(self,
|
||||||
|
data: np.ndarray,
|
||||||
|
name: str,
|
||||||
|
kind="scalar",
|
||||||
|
affine=np.identity(4)) -> None:
|
||||||
|
if data.ndim != 5:
|
||||||
|
raise Exception("Data shape order should be TXYZC")
|
||||||
|
|
||||||
|
affine = np.array(affine)
|
||||||
|
if affine.shape != (4, 4):
|
||||||
|
raise Exception("affine shape should be (4,4)")
|
||||||
|
|
||||||
|
self.data = data
|
||||||
|
self.name = name
|
||||||
|
self.kind = kind
|
||||||
|
self.affine = affine
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bioxel_size(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return z.tolist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shape(self):
|
||||||
|
return self.data.shape[1:4]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dtype(self):
|
||||||
|
return self.data.dtype
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return t.tolist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def euler(self):
|
||||||
|
t, r, z, s = transforms3d.affines.decompose44(self.affine)
|
||||||
|
return list(transforms3d.euler.mat2euler(r))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
return float(np.min(self.data))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def frame_count(self):
|
||||||
|
return self.data.shape[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel_count(self):
|
||||||
|
return self.data.shape[-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self):
|
||||||
|
return float(np.max(self.data))
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
return copy.deepcopy(self)
|
||||||
|
|
||||||
|
def fill(self, value: float, mask: np.ndarray, smooth: int = 0):
|
||||||
|
mask_frames = ()
|
||||||
|
if mask.ndim == 4:
|
||||||
|
if mask.shape[0] != self.frame_count:
|
||||||
|
raise Exception("Mask frame count is not same as ")
|
||||||
|
for f in range(self.frame_count):
|
||||||
|
mask_frame = mask[f, :, :, :]
|
||||||
|
if smooth > 0:
|
||||||
|
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||||
|
mode="nearest",
|
||||||
|
size=smooth)
|
||||||
|
# mask_frame = scipy.median_filter(
|
||||||
|
# mask_frame.astype(np.float32), size=2)
|
||||||
|
mask_frames += (mask_frame,)
|
||||||
|
elif mask.ndim == 3:
|
||||||
|
for f in range(self.frame_count):
|
||||||
|
mask_frame = mask[:, :, :]
|
||||||
|
if smooth > 0:
|
||||||
|
mask_frame = scipy.minimum_filter(mask_frame.astype(np.float32),
|
||||||
|
mode="nearest",
|
||||||
|
size=smooth)
|
||||||
|
# mask_frame = scipy.median_filter(
|
||||||
|
# mask_frame.astype(np.float32), size=2)
|
||||||
|
mask_frames += (mask_frame,)
|
||||||
|
else:
|
||||||
|
raise Exception("Mask shape order should be TXYZ or XYZ")
|
||||||
|
|
||||||
|
_mask = np.stack(mask_frames)
|
||||||
|
_mask = np.expand_dims(_mask, axis=-1)
|
||||||
|
self.data = _mask * value + (1-_mask) * self.data
|
||||||
|
|
||||||
|
def resize(self, shape: tuple, smooth: int = 0, progress_callback=None):
|
||||||
|
if len(shape) != 3:
|
||||||
|
raise Exception("Shape must be 3 dim")
|
||||||
|
|
||||||
|
data = self.data
|
||||||
|
|
||||||
|
# # TXYZC > TXYZ
|
||||||
|
# if self.kind in ['label', 'scalar']:
|
||||||
|
# data = np.amax(data, -1)
|
||||||
|
|
||||||
|
# if self.kind in ['scalar']:
|
||||||
|
# dtype = data.dtype
|
||||||
|
# data = data.astype(np.float32)
|
||||||
|
|
||||||
|
data_frames = ()
|
||||||
|
for f in range(self.frame_count):
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(f, self.frame_count)
|
||||||
|
|
||||||
|
# frame = ski.resize(data[f, :, :, :],
|
||||||
|
# shape,
|
||||||
|
# preserve_range=True,
|
||||||
|
# anti_aliasing=data.dtype.kind != "b")
|
||||||
|
frame = data[f, :, :, :, :]
|
||||||
|
if smooth > 0:
|
||||||
|
frame = scipy.median_filter(frame.astype(np.float32),
|
||||||
|
mode="nearest",
|
||||||
|
size=smooth)
|
||||||
|
|
||||||
|
factors = np.divide(self.shape, shape)
|
||||||
|
zoom_factors = [1 / f for f in factors]
|
||||||
|
order = 0 if frame.dtype == bool else 1
|
||||||
|
frame = ndi.zoom(frame,
|
||||||
|
zoom_factors+[1.0],
|
||||||
|
mode="nearest",
|
||||||
|
grid_mode=False,
|
||||||
|
order=order)
|
||||||
|
if smooth > 0:
|
||||||
|
frame = frame.astype(self.dtype)
|
||||||
|
data_frames += (frame,)
|
||||||
|
|
||||||
|
data = np.stack(data_frames)
|
||||||
|
|
||||||
|
# if self.kind in ['scalar']:
|
||||||
|
# data = data.astype(dtype)
|
||||||
|
|
||||||
|
# TXYZ > TXYZC
|
||||||
|
# if self.kind in ['label', 'scalar']:
|
||||||
|
# data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
mat_scale = transforms3d.zooms.zfdir2aff(factors[0])
|
||||||
|
self.affine = np.dot(self.affine, mat_scale)
|
||||||
434
bioxelnodes/bioxel/parse.py
Normal file
434
bioxelnodes/bioxel/parse.py
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import SimpleITK as sitk
|
||||||
|
from pyometiff import OMETIFFReader
|
||||||
|
import mrcfile
|
||||||
|
import transforms3d
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Convert any volumetric data to 3D numpy array with order TXYZC
|
||||||
|
"""
|
||||||
|
|
||||||
|
SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
||||||
|
'.ome.tiff', '.ome.tif',
|
||||||
|
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||||
|
'.mrc', '.mrc.gz', '.map', '.map.gz',
|
||||||
|
'.bmp', '.BMP',
|
||||||
|
'.png', '.PNG',
|
||||||
|
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||||
|
'.PIC', '.pic',
|
||||||
|
'.gipl', '.gipl.gz',
|
||||||
|
'.lsm', '.LSM',
|
||||||
|
'.mnc', '.MNC',
|
||||||
|
'.mrc', '.rec',
|
||||||
|
'.mha', '.mhd',
|
||||||
|
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
||||||
|
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
||||||
|
'.nrrd', '.nhdr',
|
||||||
|
'.vtk',
|
||||||
|
'.gz']
|
||||||
|
|
||||||
|
OME_EXTS = ['.ome.tiff', '.ome.tif',
|
||||||
|
'.tif', '.TIF', '.tiff', '.TIFF']
|
||||||
|
|
||||||
|
MRC_EXTS = ['.mrc', '.mrc.gz', '.map', '.map.gz']
|
||||||
|
|
||||||
|
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA']
|
||||||
|
|
||||||
|
SEQUENCE_EXTS = ['.bmp', '.BMP',
|
||||||
|
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
||||||
|
'.tif', '.TIF', '.tiff', '.TIFF',
|
||||||
|
'.png', '.PNG',
|
||||||
|
'.mrc']
|
||||||
|
|
||||||
|
|
||||||
|
def get_ext(filepath: Path) -> str:
|
||||||
|
if filepath.name.endswith(".nii.gz"):
|
||||||
|
return ".nii.gz"
|
||||||
|
elif filepath.name.endswith(".img.gz"):
|
||||||
|
return ".img.gz"
|
||||||
|
elif filepath.name.endswith(".gipl.gz"):
|
||||||
|
return ".gipl.gz"
|
||||||
|
elif filepath.name.endswith(".ome.tiff"):
|
||||||
|
return ".ome.tiff"
|
||||||
|
elif filepath.name.endswith(".ome.tif"):
|
||||||
|
return ".ome.tif"
|
||||||
|
elif filepath.name.endswith(".mrc.gz"):
|
||||||
|
return ".mrc.gz"
|
||||||
|
elif filepath.name.endswith(".map.gz"):
|
||||||
|
return ".map.gz"
|
||||||
|
else:
|
||||||
|
return filepath.suffix
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename(filepath: Path):
|
||||||
|
ext = get_ext(filepath)
|
||||||
|
return filepath.name.removesuffix(ext)
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename_parts(filepath: Path) -> str:
|
||||||
|
def has_digits(s):
|
||||||
|
return any(char.isdigit() for char in s)
|
||||||
|
|
||||||
|
name = get_filename(filepath)
|
||||||
|
parts = name.replace(".", " ").replace("_", " ").split(" ")
|
||||||
|
skip_prefixs = ["CH", "ch", "channel"]
|
||||||
|
number_part = None
|
||||||
|
number_part_i = None
|
||||||
|
|
||||||
|
for i, part in enumerate(parts[::-1]):
|
||||||
|
if has_digits(part):
|
||||||
|
if not any([part.startswith(prefix) for prefix in skip_prefixs]):
|
||||||
|
number_part = part
|
||||||
|
number_part_i = len(parts)-i
|
||||||
|
break
|
||||||
|
|
||||||
|
if number_part is None:
|
||||||
|
return name, "", ""
|
||||||
|
|
||||||
|
prefix_parts = parts[:number_part_i-1]
|
||||||
|
prefix_parts_count = sum([len(part)+1 for part in prefix_parts])
|
||||||
|
|
||||||
|
digits = ""
|
||||||
|
suffix = ""
|
||||||
|
|
||||||
|
# Iterate through the characters in reverse order
|
||||||
|
started = False
|
||||||
|
for char in number_part[::-1]:
|
||||||
|
if char.isdigit():
|
||||||
|
started = True
|
||||||
|
# If the character is a digit, add it to the digits string
|
||||||
|
digits += char
|
||||||
|
else:
|
||||||
|
if started:
|
||||||
|
# If a non-digit character is encountered, stop the loop
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
suffix += char
|
||||||
|
|
||||||
|
digits = digits[::-1]
|
||||||
|
|
||||||
|
prefix_parts_count += len(number_part) - \
|
||||||
|
len(digits) - len(suffix)
|
||||||
|
|
||||||
|
# Reverse the digits string to get the correct order
|
||||||
|
prefix = name[:prefix_parts_count]
|
||||||
|
suffix = name[prefix_parts_count+len(digits):]
|
||||||
|
|
||||||
|
return prefix, digits, suffix
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_no_digits_name(filepath: Path) -> str:
|
||||||
|
prefix, digits, suffix = get_filename_parts(filepath)
|
||||||
|
prefix = remove_end_str(prefix, "_")
|
||||||
|
prefix = remove_end_str(prefix, ".")
|
||||||
|
prefix = remove_end_str(prefix, "-")
|
||||||
|
prefix = remove_end_str(prefix, " ")
|
||||||
|
return prefix + suffix
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_index(filepath: Path) -> int:
|
||||||
|
prefix, digits, suffix = get_filename_parts(filepath)
|
||||||
|
return int(digits) if digits != "" else 0
|
||||||
|
|
||||||
|
|
||||||
|
def collect_sequence(filepath: Path):
|
||||||
|
file_dict = {}
|
||||||
|
for f in filepath.parent.iterdir():
|
||||||
|
if f.is_file() \
|
||||||
|
and get_ext(filepath) == get_ext(f) \
|
||||||
|
and get_file_no_digits_name(filepath) == get_file_no_digits_name(f):
|
||||||
|
index = get_file_index(f)
|
||||||
|
file_dict[index] = f
|
||||||
|
|
||||||
|
# reomve isolated seq file
|
||||||
|
for key in file_dict.copy().keys():
|
||||||
|
if not file_dict.get(key+1) \
|
||||||
|
and not file_dict.get(key-1):
|
||||||
|
del file_dict[key]
|
||||||
|
|
||||||
|
file_dict = dict(sorted(file_dict.items()))
|
||||||
|
sequence = [str(f) for f in file_dict.values()]
|
||||||
|
|
||||||
|
if len(sequence) == 0:
|
||||||
|
sequence = [str(filepath)]
|
||||||
|
|
||||||
|
return sequence
|
||||||
|
|
||||||
|
|
||||||
|
def remove_end_str(string: str, end: str):
|
||||||
|
while string.endswith(end) and len(string) > 0:
|
||||||
|
string = string.removesuffix(end)
|
||||||
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
def parse_volumetric_data(data_file: str, series_id="", progress_callback=None):
|
||||||
|
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_file (str): file path
|
||||||
|
series_id (str, optional): DICOM series id. Defaults to "".
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_type_: _description_
|
||||||
|
"""
|
||||||
|
|
||||||
|
data_path = Path(data_file).resolve()
|
||||||
|
ext = get_ext(data_path)
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0.0, "Reading the Data...")
|
||||||
|
|
||||||
|
is_sequence = False
|
||||||
|
if ext in SEQUENCE_EXTS:
|
||||||
|
sequence = collect_sequence(data_path)
|
||||||
|
if len(sequence) > 1:
|
||||||
|
is_sequence = True
|
||||||
|
|
||||||
|
data = None
|
||||||
|
name = "",
|
||||||
|
description = ""
|
||||||
|
affine = np.identity(4)
|
||||||
|
spacing = (1, 1, 1)
|
||||||
|
origin = (0, 0, 0)
|
||||||
|
direction = (1, 0, 0, 0, 1, 0, 0, 0, 1)
|
||||||
|
|
||||||
|
# Parsing with mrcfile
|
||||||
|
if data is None and ext in MRC_EXTS and not is_sequence:
|
||||||
|
print("Parsing with mrcfile...")
|
||||||
|
# TODO: much to do with mrc
|
||||||
|
with mrcfile.open(data_path, 'r') as mrc:
|
||||||
|
data = mrc.data
|
||||||
|
# mrc.print_header()
|
||||||
|
# print(data.shape)
|
||||||
|
# print(mrc.voxel_size)
|
||||||
|
|
||||||
|
if mrc.is_single_image():
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_image_stack():
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_volume():
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif mrc.is_volume_stack():
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
name = get_file_no_digits_name(data_path)
|
||||||
|
spacing = (mrc.voxel_size.x,
|
||||||
|
mrc.voxel_size.y,
|
||||||
|
mrc.voxel_size.z)
|
||||||
|
|
||||||
|
# Parsing with OMETIFFReader
|
||||||
|
if data is None and ext in OME_EXTS and not is_sequence:
|
||||||
|
print("Parsing with OMETIFFReader...")
|
||||||
|
reader = OMETIFFReader(fpath=data_path)
|
||||||
|
ome_image, metadata, xml_metadata = reader.read()
|
||||||
|
|
||||||
|
# TODO: some old bio-format tiff the header is not the same.
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# print(ome_image.shape)
|
||||||
|
# for key in metadata:
|
||||||
|
# print(f"{key},{metadata[key]}")
|
||||||
|
ome_order = metadata['DimOrder BF Array']
|
||||||
|
if ome_image.ndim == 2:
|
||||||
|
ome_order = ome_order.replace("T", "")\
|
||||||
|
.replace("C", "").replace("Z", "")
|
||||||
|
bioxel_order = (ome_order.index('X'),
|
||||||
|
ome_order.index('Y'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
elif ome_image.ndim == 3:
|
||||||
|
# -> XYZC
|
||||||
|
ome_order = ome_order.replace("T", "").replace("C", "")
|
||||||
|
bioxel_order = (ome_order.index('X'),
|
||||||
|
ome_order.index('Y'),
|
||||||
|
ome_order.index('Z'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
elif ome_image.ndim == 4:
|
||||||
|
# -> XYZC
|
||||||
|
ome_order = ome_order.replace("T", "")
|
||||||
|
bioxel_order = (ome_order.index('X'),
|
||||||
|
ome_order.index('Y'),
|
||||||
|
ome_order.index('Z'),
|
||||||
|
ome_order.index('C'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
elif ome_image.ndim == 5:
|
||||||
|
# -> TXYZC
|
||||||
|
bioxel_order = (ome_order.index('T'),
|
||||||
|
ome_order.index('X'),
|
||||||
|
ome_order.index('Y'),
|
||||||
|
ome_order.index('Z'),
|
||||||
|
ome_order.index('C'))
|
||||||
|
data = np.transpose(ome_image, bioxel_order)
|
||||||
|
|
||||||
|
try:
|
||||||
|
spacing = (metadata['PhysicalSizeX'],
|
||||||
|
metadata['PhysicalSizeY'],
|
||||||
|
metadata['PhysicalSizeZ'])
|
||||||
|
except:
|
||||||
|
...
|
||||||
|
|
||||||
|
name = get_file_no_digits_name(data_path)
|
||||||
|
except:
|
||||||
|
...
|
||||||
|
|
||||||
|
# Parsing with SimpleITK
|
||||||
|
if data is None:
|
||||||
|
print("Parsing with SimpleITK...")
|
||||||
|
if ext in DICOM_EXTS:
|
||||||
|
data_dirpath = data_path.parent
|
||||||
|
reader = sitk.ImageSeriesReader()
|
||||||
|
reader.MetaDataDictionaryArrayUpdateOn()
|
||||||
|
reader.LoadPrivateTagsOn()
|
||||||
|
series_files = reader.GetGDCMSeriesFileNames(
|
||||||
|
str(data_dirpath), series_id)
|
||||||
|
reader.SetFileNames(series_files)
|
||||||
|
|
||||||
|
itk_image = reader.Execute()
|
||||||
|
# for k in reader.GetMetaDataKeys(0):
|
||||||
|
# v = reader.GetMetaData(0, k)
|
||||||
|
# print(f'({k}) = = "{v}"')
|
||||||
|
|
||||||
|
def get_meta(key):
|
||||||
|
try:
|
||||||
|
stirng = reader.GetMetaData(0, key).removesuffix(" ")
|
||||||
|
if stirng in ["No study description",
|
||||||
|
"No series description",
|
||||||
|
""]:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return stirng
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
study_description = get_meta("0008|1030")
|
||||||
|
series_description = get_meta("0008|103e")
|
||||||
|
series_modality = get_meta("0008|0060")
|
||||||
|
|
||||||
|
name = study_description or data_dirpath.name
|
||||||
|
if series_description and series_modality:
|
||||||
|
description = f"{series_description}-{series_modality}"
|
||||||
|
elif series_description:
|
||||||
|
description = series_description
|
||||||
|
elif series_modality:
|
||||||
|
description = series_modality
|
||||||
|
else:
|
||||||
|
description = ""
|
||||||
|
|
||||||
|
name = name.replace(" ", "-")
|
||||||
|
description = description.replace(" ", "-")
|
||||||
|
|
||||||
|
elif ext in SEQUENCE_EXTS and is_sequence:
|
||||||
|
itk_image = sitk.ReadImage(sequence)
|
||||||
|
name = get_file_no_digits_name(data_path)
|
||||||
|
else:
|
||||||
|
itk_image = sitk.ReadImage(data_path)
|
||||||
|
name = get_filename(data_path)
|
||||||
|
|
||||||
|
# for key in itk_image.GetMetaDataKeys():
|
||||||
|
# print(f"{key},{itk_image.GetMetaData(key)}")
|
||||||
|
|
||||||
|
if progress_callback:
|
||||||
|
progress_callback(0.5, "Transpose to 'TXYZC'...")
|
||||||
|
|
||||||
|
if itk_image.GetDimension() == 2:
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
|
||||||
|
if data.ndim == 3:
|
||||||
|
data = np.transpose(data, (1, 0, 2))
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=-2) # expend Z
|
||||||
|
else:
|
||||||
|
data = np.transpose(data)
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend Z
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
|
||||||
|
elif itk_image.GetDimension() == 3:
|
||||||
|
if ext not in SEQUENCE_EXTS:
|
||||||
|
itk_image = sitk.DICOMOrient(itk_image, 'RAS')
|
||||||
|
# After sitk.DICOMOrient(), origin and direction will also orient base on LPS
|
||||||
|
# so we need to convert them into RAS
|
||||||
|
# affine = axis_conversion(from_forward='-Z',
|
||||||
|
# from_up='-Y',
|
||||||
|
# to_forward='-Z',
|
||||||
|
# to_up='Y').to_4x4()
|
||||||
|
|
||||||
|
affine = np.array([[-1.0000, 0.0000, 0.0000, 0.0000],
|
||||||
|
[0.0000, -1.0000, 0.0000, 0.0000],
|
||||||
|
[0.0000, 0.0000, 1.0000, 0.0000],
|
||||||
|
[0.0000, 0.0000, 0.0000, 1.0000]])
|
||||||
|
|
||||||
|
spacing = tuple(itk_image.GetSpacing())
|
||||||
|
origin = tuple(itk_image.GetOrigin())
|
||||||
|
direction = tuple(itk_image.GetDirection())
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
# transpose ijk to kji
|
||||||
|
if data.ndim == 4:
|
||||||
|
data = np.transpose(data, (2, 1, 0, 3))
|
||||||
|
else:
|
||||||
|
data = np.transpose(data)
|
||||||
|
data = np.expand_dims(data, axis=-1) # expend channel
|
||||||
|
|
||||||
|
data = np.expand_dims(data, axis=0) # expend frame
|
||||||
|
|
||||||
|
elif itk_image.GetDimension() == 4:
|
||||||
|
|
||||||
|
spacing = tuple(itk_image.GetSpacing()[:3])
|
||||||
|
origin = tuple(itk_image.GetOrigin()[:3])
|
||||||
|
# FIXME: not sure...
|
||||||
|
direction = np.array(itk_image.GetDirection())
|
||||||
|
direction = direction.reshape(3, 3) if itk_image.GetDimension() == 3 \
|
||||||
|
else direction.reshape(4, 4)
|
||||||
|
|
||||||
|
direction = direction[1:, 1:]
|
||||||
|
direction = tuple(direction.flatten())
|
||||||
|
|
||||||
|
data = sitk.GetArrayFromImage(itk_image)
|
||||||
|
|
||||||
|
if data.ndim == 5:
|
||||||
|
data = np.transpose(data, (0, 3, 2, 1, 4))
|
||||||
|
else:
|
||||||
|
data = np.transpose(data, (0, 3, 2, 1))
|
||||||
|
data = np.expand_dims(data, axis=-1)
|
||||||
|
|
||||||
|
if itk_image.GetDimension() > 5:
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
t = origin
|
||||||
|
r = np.array(direction).reshape((3, 3))
|
||||||
|
affine = np.dot(affine,
|
||||||
|
transforms3d.affines.compose(t, r, [1, 1, 1]))
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"spacing": spacing,
|
||||||
|
"affine": affine,
|
||||||
|
"xyz_shape": data.shape[1:4],
|
||||||
|
"frame_count": data.shape[0],
|
||||||
|
"channel_count": data.shape[-1],
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, meta
|
||||||
5
bioxelnodes/bioxel/scipy/__init__.py
Normal file
5
bioxelnodes/bioxel/scipy/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from ._interpolation import zoom
|
||||||
|
from ._filters import (gaussian_filter,
|
||||||
|
median_filter,
|
||||||
|
maximum_filter,
|
||||||
|
minimum_filter)
|
||||||
0
bioxelnodes/bioxelutils/__init__.py
Normal file
0
bioxelnodes/bioxelutils/__init__.py
Normal file
245
bioxelnodes/bioxelutils/common.py
Normal file
245
bioxelnodes/bioxelutils/common.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
from ast import literal_eval
|
||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||||
|
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 is_incompatible():
|
||||||
|
if get_file_prop("addon_version") is None:
|
||||||
|
for node_group in bpy.data.node_groups:
|
||||||
|
if node_group.name.startswith("BioxelNodes"):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||||
|
if addon_version[0] != VERSION[0]\
|
||||||
|
or addon_version[1] != VERSION[1]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def local_lib_not_updated():
|
||||||
|
use_local = False
|
||||||
|
for node_group in bpy.data.node_groups:
|
||||||
|
if node_group.name.startswith("BioxelNodes"):
|
||||||
|
node_group_lib = node_group.library
|
||||||
|
if node_group_lib:
|
||||||
|
abs_filepath = bpy.path.abspath(node_group_lib.filepath)
|
||||||
|
_local_lib_file = Path(abs_filepath).resolve().as_posix()
|
||||||
|
if _local_lib_file != NODE_LIB_FILEPATH.as_posix():
|
||||||
|
use_local = True
|
||||||
|
break
|
||||||
|
|
||||||
|
addon_version = literal_eval(get_file_prop("addon_version"))
|
||||||
|
not_update = addon_version != VERSION
|
||||||
|
return use_local and not_update
|
||||||
171
bioxelnodes/bioxelutils/container.py
Normal file
171
bioxelnodes/bioxelutils/container.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
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,
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
output_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
except:
|
||||||
|
output_node = node_group.nodes.new("NodeGroupOutput")
|
||||||
|
|
||||||
|
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,
|
||||||
|
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, (-600, 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)
|
||||||
|
|
||||||
|
return container_obj
|
||||||
209
bioxelnodes/bioxelutils/layer.py
Normal file
209
bioxelnodes/bioxelutils/layer.py
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import random
|
||||||
|
import re
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
import pyopenvdb 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, 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
|
||||||
68
bioxelnodes/bioxelutils/node.py
Normal file
68
bioxelnodes/bioxelutils/node.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
from .common import get_file_prop, set_file_prop
|
||||||
|
from ..exceptions import Incompatible, NoFound
|
||||||
|
|
||||||
|
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_group(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
|
||||||
|
|
||||||
|
if get_file_prop("addon_version") is None:
|
||||||
|
set_file_prop("addon_version", VERSION)
|
||||||
|
|
||||||
|
local_lib_file = None
|
||||||
|
addon_lib_file = NODE_LIB_FILEPATH.as_posix()
|
||||||
|
|
||||||
|
for node_group in bpy.data.node_groups:
|
||||||
|
if node_group.name.startswith("BioxelNodes"):
|
||||||
|
node_group_lib = node_group.library
|
||||||
|
if node_group_lib:
|
||||||
|
abs_filepath = bpy.path.abspath(node_group_lib.filepath)
|
||||||
|
_local_lib_file = Path(abs_filepath).resolve().as_posix()
|
||||||
|
if _local_lib_file != addon_lib_file:
|
||||||
|
local_lib_file = _local_lib_file
|
||||||
|
break
|
||||||
|
|
||||||
|
# local lib first
|
||||||
|
lib_file = local_lib_file or addon_lib_file
|
||||||
|
bpy.ops.wm.append('EXEC_DEFAULT',
|
||||||
|
directory=f"{lib_file}/NodeTree",
|
||||||
|
filename=node_type,
|
||||||
|
link=use_link,
|
||||||
|
use_recursive=True,
|
||||||
|
do_reuse_local_id=True)
|
||||||
|
node_group = bpy.data.node_groups.get(node_type)
|
||||||
|
|
||||||
|
if node_group is None:
|
||||||
|
raise NoFound('No custom node found')
|
||||||
|
|
||||||
|
return node_group
|
||||||
|
|
||||||
|
|
||||||
|
def assign_node_group(node, node_type: str):
|
||||||
|
node.node_tree = bpy.data.node_groups[node_type]
|
||||||
|
node.width = 200.0
|
||||||
|
node.name = node_type
|
||||||
|
return node
|
||||||
|
|
||||||
|
|
||||||
|
def add_node_to_graph(node_name: str, node_group, use_link=True):
|
||||||
|
node_type = f"BioxelNodes_{node_name}"
|
||||||
|
|
||||||
|
# Deselect all nodes first
|
||||||
|
for node in node_group.nodes:
|
||||||
|
if node.select:
|
||||||
|
node.select = False
|
||||||
|
|
||||||
|
get_node_group(node_type, use_link)
|
||||||
|
node = node_group.nodes.new("GeometryNodeGroup")
|
||||||
|
assign_node_group(node, node_type)
|
||||||
|
|
||||||
|
node.show_options = False
|
||||||
|
return node
|
||||||
37
bioxelnodes/blender_manifest.toml
Normal file
37
bioxelnodes/blender_manifest.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "bioxelnodes"
|
||||||
|
version = "1.0.0"
|
||||||
|
name = "Bioxel Nodes"
|
||||||
|
tagline = "For scientific volumetric data visualization in Blender"
|
||||||
|
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://omoolab.github.io/BioxelNodes/latest"
|
||||||
|
|
||||||
|
tags = ["Geometry Nodes", "Render", "Import-Export"]
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
license = ["SPDX:MIT"]
|
||||||
|
copyright = ["2024 OmooLab"]
|
||||||
|
platforms = ["windows-x64", "linux-x64", "macos-arm64", "macos-x64"]
|
||||||
|
|
||||||
|
wheels = [
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/SimpleITK-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-macosx_10_9_arm64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-win_amd64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-macosx_11_0_arm64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl",
|
||||||
|
"./wheels/h5py-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
|
"./wheels/tifffile-2024.7.24-py3-none-any.whl",
|
||||||
|
"./wheels/pyometiff-1.0.0-py3-none-any.whl",
|
||||||
|
"./wheels/mrcfile-1.5.1-py2.py3-none-any.whl",
|
||||||
|
"./wheels/transforms3d-0.4.2-py3-none-any.whl"
|
||||||
|
]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Import/export volume data from/to disk"
|
||||||
224
bioxelnodes/constants.py
Normal file
224
bioxelnodes/constants.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
VERSION = (1, 0, 0)
|
||||||
|
NODE_LIB_FILENAME = "BioxelNodes_v1.0.x"
|
||||||
|
NODE_LIB_FILEPATH = Path(Path(__file__).parent,
|
||||||
|
f"assets/Nodes/{NODE_LIB_FILENAME}.blend").resolve()
|
||||||
|
|
||||||
|
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 Color',
|
||||||
|
'icon': 'COLOR',
|
||||||
|
'name': 'CutoutByColor',
|
||||||
|
'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': 'Properties',
|
||||||
|
'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': 'Cutters',
|
||||||
|
'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': 'Utils',
|
||||||
|
'icon': 'MODIFIER',
|
||||||
|
'items': [
|
||||||
|
{
|
||||||
|
'label': 'Pick Surface',
|
||||||
|
'icon': 'OUTLINER_OB_MESH',
|
||||||
|
'name': 'PickSurface',
|
||||||
|
'description': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Pick Volume',
|
||||||
|
'icon': 'OUTLINER_OB_VOLUME',
|
||||||
|
'name': 'PickVolume',
|
||||||
|
'description': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Pick Shape Wire',
|
||||||
|
'icon': 'FILE_VOLUME',
|
||||||
|
'name': 'PickShapeWire',
|
||||||
|
'description': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Pick Bbox Wire',
|
||||||
|
'icon': 'MESH_CUBE',
|
||||||
|
'name': 'PickBboxWire',
|
||||||
|
'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': ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1 +0,0 @@
|
|||||||
from .menus import CustomNodes
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import shutil
|
|
||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
from .nodes import AddCustomNode
|
|
||||||
|
|
||||||
|
|
||||||
class CustomNodes():
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
menu_items,
|
|
||||||
nodes_file,
|
|
||||||
class_prefix="CUSTOMNODES_MT_NODES",
|
|
||||||
root_label='CustomNodes',
|
|
||||||
root_icon='NONE'
|
|
||||||
) -> None:
|
|
||||||
if not Path(nodes_file).is_file():
|
|
||||||
raise FileNotFoundError(str(Path(nodes_file).resolve()))
|
|
||||||
|
|
||||||
self.menu_items = menu_items
|
|
||||||
self.nodes_file = str(Path(nodes_file).resolve())
|
|
||||||
self.class_prefix = class_prefix
|
|
||||||
self.root_label = root_label
|
|
||||||
self.root_icon = root_icon
|
|
||||||
self.class_prefix = class_prefix
|
|
||||||
|
|
||||||
menu_classes = []
|
|
||||||
self._create_menu_class(
|
|
||||||
items=menu_items,
|
|
||||||
label=root_label,
|
|
||||||
menu_classes=menu_classes
|
|
||||||
)
|
|
||||||
self.menu_classes = menu_classes
|
|
||||||
|
|
||||||
idname = f"{class_prefix}_{root_label.replace(' ', '').upper()}"
|
|
||||||
|
|
||||||
def add_node_menu(self, context):
|
|
||||||
if ('GeometryNodeTree' == bpy.context.area.spaces[0].tree_type):
|
|
||||||
layout = self.layout
|
|
||||||
layout.separator()
|
|
||||||
layout.menu(idname, icon=root_icon)
|
|
||||||
|
|
||||||
self.add_node_menu = add_node_menu
|
|
||||||
|
|
||||||
def _create_menu_class(self, menu_classes, items, label='CustomNodes', icon=0, idname_namespace=None):
|
|
||||||
nodes_file = self.nodes_file
|
|
||||||
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
|
|
||||||
nodes_file = self.nodes_file
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
# print(item)
|
|
||||||
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(
|
|
||||||
'customnodes.add_custom_node',
|
|
||||||
text=item.get('label'),
|
|
||||||
icon=item.get('icon') or 'NONE'
|
|
||||||
)
|
|
||||||
op.nodes_file = nodes_file
|
|
||||||
op.node_type = item['node_type']
|
|
||||||
op.node_label = item.get('label') or ""
|
|
||||||
op.node_description = item.get(
|
|
||||||
'node_description') or "Add Custom Node."
|
|
||||||
op.node_link = item.get('link') or True
|
|
||||||
op.node_callback = item.get('node_callback') or ""
|
|
||||||
|
|
||||||
menu_classes.append(Menu)
|
|
||||||
return Menu
|
|
||||||
|
|
||||||
def _find_item(self, found_items, menu_items, node_type: str):
|
|
||||||
|
|
||||||
for item in menu_items:
|
|
||||||
if item == 'separator':
|
|
||||||
continue
|
|
||||||
|
|
||||||
if item.get("node_type") == node_type:
|
|
||||||
found_items.append(item)
|
|
||||||
|
|
||||||
item_items = item.get('items')
|
|
||||||
if item_items:
|
|
||||||
self._find_item(found_items, item_items, node_type)
|
|
||||||
|
|
||||||
def find_item(self, node_type: str):
|
|
||||||
found_items = []
|
|
||||||
self._find_item(found_items, self.menu_items, node_type)
|
|
||||||
return found_items[0] if len(found_items) > 0 else None
|
|
||||||
|
|
||||||
def add_node(self, node_group, node_type: str):
|
|
||||||
item = self.find_item(node_type)
|
|
||||||
op = AddCustomNode()
|
|
||||||
op.nodes_file = self.nodes_file
|
|
||||||
op.node_type = node_type
|
|
||||||
if item:
|
|
||||||
op.node_label = item.get('label') or ""
|
|
||||||
op.node_link = item.get('link') or True
|
|
||||||
op.node_callback = item.get('node_callback') or ""
|
|
||||||
else:
|
|
||||||
op.node_label = ""
|
|
||||||
op.node_link = True
|
|
||||||
op.node_callback = ""
|
|
||||||
return op.add_node(node_group)
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
for cls in self.menu_classes:
|
|
||||||
bpy.utils.register_class(cls)
|
|
||||||
|
|
||||||
bpy.types.NODE_MT_add.append(self.add_node_menu)
|
|
||||||
bpy.types.Scene.customnodes_node_dir = bpy.props.StringProperty(
|
|
||||||
name="Node Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default="//"
|
|
||||||
)
|
|
||||||
|
|
||||||
def unregister(self):
|
|
||||||
bpy.types.NODE_MT_add.remove(self.add_node_menu)
|
|
||||||
try:
|
|
||||||
for cls in reversed(self.menu_classes):
|
|
||||||
bpy.utils.unregister_class(cls)
|
|
||||||
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import bpy
|
|
||||||
|
|
||||||
|
|
||||||
class AddCustomNode():
|
|
||||||
|
|
||||||
nodes_file: bpy.props.StringProperty(
|
|
||||||
name="nodes_file",
|
|
||||||
subtype='FILE_PATH',
|
|
||||||
default=""
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
node_type: bpy.props.StringProperty(
|
|
||||||
name='node_type',
|
|
||||||
description='',
|
|
||||||
default='',
|
|
||||||
subtype='NONE',
|
|
||||||
maxlen=0
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
node_label: bpy.props.StringProperty(
|
|
||||||
name='node_label',
|
|
||||||
default=''
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
node_description: bpy.props.StringProperty(
|
|
||||||
name="node_description",
|
|
||||||
description="",
|
|
||||||
default="Add custom node group.",
|
|
||||||
subtype="NONE"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
node_link: bpy.props.BoolProperty(
|
|
||||||
name='node_link',
|
|
||||||
default=True
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
node_callback: bpy.props.StringProperty(
|
|
||||||
name='node_callback',
|
|
||||||
default=''
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def description(cls, context, properties):
|
|
||||||
return properties.node_description
|
|
||||||
|
|
||||||
def assign_node_tree(self, node):
|
|
||||||
node.node_tree = bpy.data.node_groups[self.node_type]
|
|
||||||
|
|
||||||
node.width = 200.0
|
|
||||||
node.label = self.node_label or self.node_type
|
|
||||||
node.name = self.node_type
|
|
||||||
|
|
||||||
if self.node_callback:
|
|
||||||
exec(self.node_callback)
|
|
||||||
|
|
||||||
return node
|
|
||||||
|
|
||||||
def get_node_tree(self, node_type, node_link):
|
|
||||||
# try to get node from current file if exists
|
|
||||||
node_tree = bpy.data.node_groups.get(node_type)
|
|
||||||
# if not exists, get it from asset file.
|
|
||||||
if not node_tree:
|
|
||||||
bpy.ops.wm.append(
|
|
||||||
'EXEC_DEFAULT',
|
|
||||||
directory=f"{self.nodes_file}/NodeTree",
|
|
||||||
filename=node_type,
|
|
||||||
link=node_link,
|
|
||||||
use_recursive=True
|
|
||||||
)
|
|
||||||
|
|
||||||
node_tree = bpy.data.node_groups.get(node_type)
|
|
||||||
if node_tree:
|
|
||||||
# self.recursive_append_material(node_tree)
|
|
||||||
return node_tree
|
|
||||||
else:
|
|
||||||
raise RuntimeError('No custom node found')
|
|
||||||
|
|
||||||
def recursive_append_material(self, node_tree):
|
|
||||||
for child in node_tree.nodes:
|
|
||||||
material_socket = child.inputs.get('Material')
|
|
||||||
if material_socket:
|
|
||||||
print(material_socket.default_value)
|
|
||||||
try:
|
|
||||||
self.recursive_append_material(child.node_tree)
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
def add_node(self, node_group):
|
|
||||||
# Deselect all nodes first
|
|
||||||
for node in node_group.nodes:
|
|
||||||
if node.select:
|
|
||||||
node.select = False
|
|
||||||
|
|
||||||
self.get_node_tree(self.node_type, self.node_link)
|
|
||||||
node = node_group.nodes.new("GeometryNodeGroup")
|
|
||||||
self.assign_node_tree(node)
|
|
||||||
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
class CUSTOMNODES_OT_Add_Custom_Node(bpy.types.Operator, AddCustomNode):
|
|
||||||
bl_idname = "customnodes.add_custom_node"
|
|
||||||
bl_label = "Add Custom Node"
|
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
self.get_node_tree(self.node_type, self.node_link)
|
|
||||||
|
|
||||||
# intended to be called upon button press in the node tree
|
|
||||||
prev_context = bpy.context.area.type
|
|
||||||
bpy.context.area.type = 'NODE_EDITOR'
|
|
||||||
# actually invoke the operator to add a node to the current node tree
|
|
||||||
# use_transform=True ensures it appears where the user's mouse is and is currently
|
|
||||||
# being moved so the user can place it where they wish
|
|
||||||
bpy.ops.node.add_node(
|
|
||||||
'INVOKE_DEFAULT',
|
|
||||||
type='GeometryNodeGroup',
|
|
||||||
use_transform=True
|
|
||||||
)
|
|
||||||
bpy.context.area.type = prev_context
|
|
||||||
node = bpy.context.active_node
|
|
||||||
|
|
||||||
self.assign_node_tree(node)
|
|
||||||
|
|
||||||
return {"FINISHED"}
|
|
||||||
@@ -2,3 +2,21 @@ class CancelledByUser(Exception):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
message = 'Cancelled by user'
|
message = 'Cancelled by user'
|
||||||
super().__init__(message)
|
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
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
from .preferences import ExternalPackagePreferences
|
|
||||||
from .package import PackageInstaller
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
"""
|
|
||||||
Handling installation of external python packages inside of Blender's bundled python.
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import logging
|
|
||||||
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ADDON_NAME = __package__.split(".")[0]
|
|
||||||
|
|
||||||
PYPI_MIRROR = {
|
|
||||||
# the original.
|
|
||||||
'Default': '',
|
|
||||||
# two mirrors in China Mainland to help those poor victims under GFW.
|
|
||||||
'BFSU (Beijing)': 'https://mirrors.bfsu.edu.cn/pypi/web/simple',
|
|
||||||
'TUNA (Beijing)': 'https://pypi.tuna.tsinghua.edu.cn/simple',
|
|
||||||
# append more if necessary.
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class InstallationError(Exception):
|
|
||||||
"""
|
|
||||||
Exception raised when there is an error installing a package.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
package_name : str
|
|
||||||
The name of the package that failed to install.
|
|
||||||
error_message : str
|
|
||||||
The error message returned by pip.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, package_name, error_message):
|
|
||||||
self.package_name = package_name
|
|
||||||
self.error_message = error_message
|
|
||||||
super().__init__(f"Failed to install {package_name}: {error_message}")
|
|
||||||
|
|
||||||
|
|
||||||
class PackageInstaller():
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
requirements_dir: str,
|
|
||||||
log_dir: str = None,
|
|
||||||
pypi_mirror_provider='Default',
|
|
||||||
) -> None:
|
|
||||||
self.pypi_mirror_provider = pypi_mirror_provider
|
|
||||||
self.log_path: Path = Path(log_dir) if log_dir \
|
|
||||||
else Path.home() / '.externalpackage' / 'logs'
|
|
||||||
self.requirements_path: Path = Path(requirements_dir)
|
|
||||||
|
|
||||||
def start_logging(self, logfile_name: str = 'side-packages-install') -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Configure and start logging to a file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
logfile_name : str, optional
|
|
||||||
The name of the log file. Defaults to 'side-packages-install'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
logging.Logger
|
|
||||||
A Logger object that can be used to write log messages.
|
|
||||||
|
|
||||||
This function sets up a logging configuration with a specified log file name and logging level.
|
|
||||||
The log file will be created in the `ADDON_DIR/logs` directory. If the directory
|
|
||||||
does not exist, it will be created. The function returns a Logger object that can be used to
|
|
||||||
write log messages.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Create the logs directory if it doesn't exist
|
|
||||||
self.log_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
# Set up logging configuration
|
|
||||||
logfile_path = Path(self.log_path, f"{logfile_name}.log")
|
|
||||||
logging.basicConfig(filename=logfile_path,
|
|
||||||
level=logging.INFO, encoding='utf-8')
|
|
||||||
|
|
||||||
# Return logger object
|
|
||||||
return logging.getLogger()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pypi_mirror_url(self) -> str:
|
|
||||||
"""
|
|
||||||
Process a PyPI mirror provider and return the corresponding URL.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pypi_mirror_provider : str
|
|
||||||
The PyPI mirror provider to process.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
The URL of the PyPI mirror.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
ValueError
|
|
||||||
If the provided PyPI mirror provider is invalid.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.pypi_mirror_provider.startswith('https:'):
|
|
||||||
return self.pypi_mirror_provider
|
|
||||||
elif self.pypi_mirror_provider in PYPI_MIRROR.keys():
|
|
||||||
return PYPI_MIRROR[self.pypi_mirror_provider]
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid PyPI mirror provider: {self.pypi_mirror_provider}")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def packages(self) -> dict:
|
|
||||||
"""
|
|
||||||
Read a requirements file and extract package information into a dictionary.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
requirements : str, optional
|
|
||||||
The path to the requirements file. If not provided, the function looks for a `requirements.txt`
|
|
||||||
file in the same directory as the script.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
dict
|
|
||||||
A dictionary containing package information. Each element of the dictionary is a dictionary containing the package name, version, and description.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
Given the following requirements file:
|
|
||||||
```python
|
|
||||||
Flask==1.1.2 # A micro web framework for Python
|
|
||||||
pandas==1.2.3 # A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool
|
|
||||||
numpy==1.20.1 # Fundamental package for scientific computing
|
|
||||||
```
|
|
||||||
The function would return the following dictionary:
|
|
||||||
```python
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"package": "Flask",
|
|
||||||
"version": "1.1.2",
|
|
||||||
"description": "A micro web framework for Python"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "pandas",
|
|
||||||
"version": "1.2.3",
|
|
||||||
"description": "A fast, powerful, flexible, and easy-to-use data analysis and manipulation tool"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"package": "numpy",
|
|
||||||
"version": "1.20.1",
|
|
||||||
"description": "Fundamental package for scientific computing"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
requirements_filepath = self.requirements_path / "requirements.txt"
|
|
||||||
if requirements_filepath.is_file():
|
|
||||||
with requirements_filepath.open('r') as f:
|
|
||||||
lines = f.read().splitlines()
|
|
||||||
packages = {}
|
|
||||||
for line in lines:
|
|
||||||
try:
|
|
||||||
package, description = line.split('#')
|
|
||||||
package_meta = package.split('==')
|
|
||||||
name = package_meta[0].strip()
|
|
||||||
packages[name] = {
|
|
||||||
"name": name,
|
|
||||||
"version": package_meta[1].strip(),
|
|
||||||
"description": description.strip()
|
|
||||||
}
|
|
||||||
except ValueError:
|
|
||||||
# Skip line if it doesn't have the expected format
|
|
||||||
pass
|
|
||||||
return packages
|
|
||||||
else:
|
|
||||||
raise FileNotFoundError(requirements_filepath)
|
|
||||||
|
|
||||||
def is_installed(self, package_name: str) -> bool:
|
|
||||||
"""
|
|
||||||
Check if the specified package is installed and the version matches that specified
|
|
||||||
in the `requirements.txt` file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
package : str
|
|
||||||
The name of the package to check.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bool
|
|
||||||
True if the package is the current version, False otherwise.
|
|
||||||
|
|
||||||
"""
|
|
||||||
package = self.packages.get(package_name)
|
|
||||||
try:
|
|
||||||
available_version = get_version(package['name'])
|
|
||||||
return available_version == package['version']
|
|
||||||
except PackageNotFoundError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run_python(self, cmd_list: list = None, timeout: int = 600):
|
|
||||||
"""
|
|
||||||
Runs pip command using the specified command list and returns the command output.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
cmd_list : list, optional
|
|
||||||
List of pip commands to be executed. Defaults to None.
|
|
||||||
mirror_url : str, optional
|
|
||||||
URL of a package repository mirror to be used for the command. Defaults to ''.
|
|
||||||
timeout : int, optional
|
|
||||||
Time in seconds to wait for the command to complete. Defaults to 600.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
tuple
|
|
||||||
A tuple containing the command list, command return code, command standard output,
|
|
||||||
and command standard error.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
Install numpy using pip and print the command output
|
|
||||||
```python
|
|
||||||
cmd_list = ["-m", "pip", "install", "numpy"]
|
|
||||||
mirror_url = 'https://pypi.org/simple/'
|
|
||||||
cmd_output = run_python(cmd_list, timeout=300)
|
|
||||||
print(cmd_output)
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# path to python.exe
|
|
||||||
python_exe = str(Path(sys.executable).resolve())
|
|
||||||
|
|
||||||
# build the command list
|
|
||||||
cmd_list = [python_exe] + cmd_list
|
|
||||||
|
|
||||||
# add mirror to the command list if it's valid
|
|
||||||
if self.pypi_mirror_url and self.pypi_mirror_url.startswith('https'):
|
|
||||||
cmd_list += ['-i', self.pypi_mirror_url]
|
|
||||||
|
|
||||||
log = self.start_logging()
|
|
||||||
log.info(f"Running Pip: '{cmd_list}'")
|
|
||||||
|
|
||||||
# run the command and capture the output
|
|
||||||
result = subprocess.run(cmd_list, timeout=timeout,
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if result.returncode != 0:
|
|
||||||
log.error('Command failed: %s', cmd_list)
|
|
||||||
log.error('stdout: %s', result.stdout.decode())
|
|
||||||
log.error('stderr: %s', result.stderr.decode(errors='ignore'))
|
|
||||||
else:
|
|
||||||
log.info('Command succeeded: %s', cmd_list)
|
|
||||||
log.info('stdout: %s', result.stdout.decode())
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
# return the command list, return code, stdout, and stderr as a tuple
|
|
||||||
return result
|
|
||||||
|
|
||||||
def install_package(self, package: str) -> list:
|
|
||||||
"""
|
|
||||||
Install a Python package and its dependencies using pip.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
package : str
|
|
||||||
The name of the package to install.
|
|
||||||
pypi_mirror_provider : str, optional
|
|
||||||
The name/url of the PyPI mirror provider to use. Default is 'Default'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list
|
|
||||||
A list of tuples containing the command list, return code, stdout, and stderr
|
|
||||||
for each pip command run.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
ValueError
|
|
||||||
If the package name is not provided.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
To install the package 'requests' from the PyPI mirror 'MyMirror', use:
|
|
||||||
```
|
|
||||||
install_package('requests', 'MyMirror')
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not package:
|
|
||||||
raise ValueError("Package name must be provided.")
|
|
||||||
|
|
||||||
print(f"Installing {package}...")
|
|
||||||
print(f"Using PyPI mirror: {self.pypi_mirror_provider}")
|
|
||||||
|
|
||||||
self.run_python(["-m", "ensurepip"]),
|
|
||||||
self.run_python(["-m", "pip", "install", "--upgrade", "pip"])
|
|
||||||
result = self.run_python(["-m", "pip", "install", package, "--user"])
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def install_all_packages(self, pypi_mirror_provider: str = 'Default') -> list:
|
|
||||||
"""
|
|
||||||
Install all packages listed in the 'requirements.txt' file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pypi_mirror_provider : str, optional
|
|
||||||
The PyPI mirror to use for package installation. Defaults to 'Default',
|
|
||||||
which uses the official PyPI repository.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list
|
|
||||||
A list of tuples containing the installation results for each package.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
InstallationError
|
|
||||||
If there is an error during package installation.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
To install all packages listed in the 'requirements.txt' file, run the following command:
|
|
||||||
```
|
|
||||||
install_all_packages(pypi_mirror_provider='https://pypi.org/simple/')
|
|
||||||
```
|
|
||||||
|
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
for package in self.packages.items():
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.install_package(
|
|
||||||
package=f"{package.get('name')}=={package.get('version')}"
|
|
||||||
)
|
|
||||||
results.append(result)
|
|
||||||
except InstallationError as e:
|
|
||||||
raise InstallationError(
|
|
||||||
f"Error installing package {package.get('name')}: {str(e)}")
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
class EXTERNALPACKAGE_OT_Install_Package(bpy.types.Operator):
|
|
||||||
bl_idname = 'externalpackage.install_package'
|
|
||||||
bl_label = 'Install Given Python Package'
|
|
||||||
bl_options = {'REGISTER', 'INTERNAL'}
|
|
||||||
|
|
||||||
package: bpy.props.StringProperty(
|
|
||||||
name='Python Package',
|
|
||||||
description='Python Package to Install'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
version: bpy.props.StringProperty(
|
|
||||||
name='Python Package',
|
|
||||||
description='Python Package to Install'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
description: bpy.props.StringProperty(
|
|
||||||
name='Operator description',
|
|
||||||
default='Install specified python package.'
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def description(cls, context, properties):
|
|
||||||
return properties.description
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
preferences = context.preferences.addons[ADDON_NAME].preferences
|
|
||||||
installer = PackageInstaller(
|
|
||||||
pypi_mirror_provider=preferences.pypi_mirror_provider,
|
|
||||||
log_dir=preferences.log_dir,
|
|
||||||
requirements_dir=preferences.requirements_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
result = installer.install_package(f"{self.package}=={self.version}")
|
|
||||||
if result.returncode == 0 or installer.is_installed(self.package):
|
|
||||||
self.report(
|
|
||||||
{'INFO'},
|
|
||||||
f"Successfully installed {self.package} v{self.version}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.report(
|
|
||||||
{'ERROR'},
|
|
||||||
f"""Error installing package. Please check the log files
|
|
||||||
in {preferences.log_dir}."""
|
|
||||||
)
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
|
||||||
from .package import PackageInstaller, PYPI_MIRROR
|
|
||||||
|
|
||||||
# Defines the preferences panel for the addon, which shows the buttons for
|
|
||||||
# installing and reinstalling the required python packages defined in 'requirements.txt'
|
|
||||||
|
|
||||||
|
|
||||||
def get_pypi_mirrors(self, context, edit_text):
|
|
||||||
return PYPI_MIRROR.keys()
|
|
||||||
|
|
||||||
|
|
||||||
class RebootBlender(bpy.types.Operator):
|
|
||||||
bl_idname = "externalpackage.reboot_blender"
|
|
||||||
bl_label = "Reboot Blender"
|
|
||||||
bl_description = "Reboot Blender"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.invoke_confirm(self, event)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
blender_launcher = Path(
|
|
||||||
bpy.app.binary_path).parent / "blender-launcher.exe"
|
|
||||||
subprocess.run([str(blender_launcher), "-con", "--python-expr",
|
|
||||||
"import bpy; bpy.ops.wm.recover_last_session()"])
|
|
||||||
bpy.ops.wm.quit_blender()
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ExternalPackagePreferences():
|
|
||||||
requirements_dir: bpy.props.StringProperty(
|
|
||||||
name="requirements.txt Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(__file__).parent)
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
log_dir: bpy.props.StringProperty(
|
|
||||||
name="Python Log Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(Path.home(), '.externalpackage', 'logs'))
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
pypi_mirror_provider: bpy.props.StringProperty(
|
|
||||||
name='pypi_mirror_provider',
|
|
||||||
description='PyPI Mirror Provider',
|
|
||||||
options={'TEXTEDIT_UPDATE', 'LIBRARY_EDITABLE'},
|
|
||||||
default='Default',
|
|
||||||
subtype='NONE',
|
|
||||||
search=get_pypi_mirrors,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(
|
|
||||||
text='**Install the required packages, then hit "Reboot Blender"**')
|
|
||||||
|
|
||||||
col_main = layout.column(heading='', align=False)
|
|
||||||
row_import = col_main.row()
|
|
||||||
row_import.prop(self, 'pypi_mirror_provider', text='PyPI Mirror')
|
|
||||||
|
|
||||||
installer = PackageInstaller(
|
|
||||||
pypi_mirror_provider=self.pypi_mirror_provider,
|
|
||||||
log_dir=self.log_dir,
|
|
||||||
requirements_dir=self.requirements_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
for package in installer.packages.values():
|
|
||||||
|
|
||||||
name = package.get('name')
|
|
||||||
version = package.get('version')
|
|
||||||
description = package.get('description')
|
|
||||||
|
|
||||||
if installer.is_installed(name):
|
|
||||||
row = layout.row()
|
|
||||||
row.label(text=f"{name} version {version} is installed.")
|
|
||||||
op = row.operator('externalpackage.install_package',
|
|
||||||
text=f'Reinstall {name}')
|
|
||||||
op.package = name
|
|
||||||
op.version = version
|
|
||||||
op.description = f'Reinstall {name}'
|
|
||||||
else:
|
|
||||||
row = layout.row(heading=f"Package: {name}")
|
|
||||||
col = row.column()
|
|
||||||
col.label(text=str(description))
|
|
||||||
col = row.column()
|
|
||||||
op = col.operator('externalpackage.install_package',
|
|
||||||
text=f'Install {name}')
|
|
||||||
op.package = name
|
|
||||||
op.version = version
|
|
||||||
op.description = f'Install required python package: {name}'
|
|
||||||
|
|
||||||
layout.operator(RebootBlender.bl_idname)
|
|
||||||
@@ -1,953 +0,0 @@
|
|||||||
import math
|
|
||||||
import bpy
|
|
||||||
import shutil
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
from bpy_extras.io_utils import axis_conversion
|
|
||||||
import pyopenvdb as vdb
|
|
||||||
import numpy as np
|
|
||||||
from pathlib import Path
|
|
||||||
import mathutils
|
|
||||||
import random
|
|
||||||
|
|
||||||
from . import skimage as ski
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
from .exceptions import CancelledByUser
|
|
||||||
from .props import BIOXELNODES_Series
|
|
||||||
from .parse import DICOM_EXTS, FH_EXTS, SUPPORT_EXTS, get_ext, parse_volumetric_data
|
|
||||||
from .utils import (calc_bbox_verts, get_all_layers, get_container_from_selection, get_layer,
|
|
||||||
get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, progress_update, progress_bar, save_vdb, save_vdbs, select_object)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
import SimpleITK as sitk
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
|
||||||
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
|
||||||
int(orig_shape[1] / bioxel_size * orig_spacing[1]),
|
|
||||||
int(orig_shape[2] / bioxel_size * orig_spacing[2]))
|
|
||||||
|
|
||||||
return (shape[0] if shape[0] > 0 else 1,
|
|
||||||
shape[1] if shape[1] > 0 else 1,
|
|
||||||
shape[2] if shape[2] > 0 else 1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0):
|
|
||||||
size = (float(shape[0] * bioxel_size * scale),
|
|
||||||
float(shape[1] * bioxel_size * scale),
|
|
||||||
float(shape[2] * bioxel_size * scale))
|
|
||||||
|
|
||||||
return size
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
ImportVolumetricData
|
|
||||||
-> ParseVolumetricData -> ImportVolumetricDataDialog
|
|
||||||
FH_ImportVolumetricData
|
|
||||||
|
|
||||||
start import parse data execute import
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ImportVolumetricData():
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
||||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
|
||||||
|
|
||||||
read_as = "scalar"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) > 0:
|
|
||||||
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
directory=self.directory,
|
|
||||||
container=containers[0].name,
|
|
||||||
read_as=self.read_as)
|
|
||||||
else:
|
|
||||||
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
directory=self.directory,
|
|
||||||
read_as=self.read_as)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.fileselect_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class ImportAsScalarLayer(bpy.types.Operator, ImportVolumetricData):
|
|
||||||
bl_idname = "bioxelnodes.import_as_scalar_layer"
|
|
||||||
bl_label = "Import as Scalar"
|
|
||||||
bl_description = "Import Volumetric Data to Container as Scalar"
|
|
||||||
read_as = "scalar"
|
|
||||||
|
|
||||||
|
|
||||||
class ImportAsLabelLayer(bpy.types.Operator, ImportVolumetricData):
|
|
||||||
bl_idname = "bioxelnodes.import_as_label_layer"
|
|
||||||
bl_label = "Import as Label"
|
|
||||||
bl_description = "Import Volumetric Data to Container as Label"
|
|
||||||
read_as = "label"
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
|
||||||
bl_idname = "BIOXELNODES_FH_ImportVolumetricData"
|
|
||||||
bl_label = "File handler for dicom import"
|
|
||||||
bl_import_operator = "bioxelnodes.parse_volumetric_data"
|
|
||||||
bl_file_extensions = ";".join(FH_EXTS)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll_drop(cls, context):
|
|
||||||
return (context.area and context.area.type == 'VIEW_3D')
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def get_series_ids(self, context):
|
|
||||||
items = []
|
|
||||||
for index, series_id in enumerate(self.series_ids):
|
|
||||||
items.append((
|
|
||||||
series_id.id,
|
|
||||||
series_id.label,
|
|
||||||
"",
|
|
||||||
index
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
class ParseVolumetricData(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.parse_volumetric_data"
|
|
||||||
bl_label = "Import Volumetric Data"
|
|
||||||
bl_description = "Import Volumetric Data as Layer"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
meta = {}
|
|
||||||
thread = None
|
|
||||||
_timer = None
|
|
||||||
|
|
||||||
progress: bpy.props.FloatProperty(name="Progress",
|
|
||||||
options={"SKIP_SAVE"},
|
|
||||||
default=1) # type: ignore
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
||||||
directory: bpy.props.StringProperty(subtype='DIR_PATH') # type: ignore
|
|
||||||
container: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
read_as: bpy.props.EnumProperty(name="Read as",
|
|
||||||
default="scalar",
|
|
||||||
items=[("scalar", "Scalar", ""),
|
|
||||||
("label", "Labels", "")]) # type: ignore
|
|
||||||
|
|
||||||
series_id: bpy.props.EnumProperty(name="Select Series",
|
|
||||||
items=get_series_ids) # type: ignore
|
|
||||||
|
|
||||||
series_ids: bpy.props.CollectionProperty(
|
|
||||||
type=BIOXELNODES_Series) # type: ignore
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
ext = get_ext(self.filepath)
|
|
||||||
if ext not in SUPPORT_EXTS:
|
|
||||||
self.report({"WARNING"}, "Not supported extension.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
print("Collecting Meta Data...")
|
|
||||||
|
|
||||||
def parse_volumetric_data_func(self, context, cancel):
|
|
||||||
|
|
||||||
def progress_callback(factor, text):
|
|
||||||
if cancel():
|
|
||||||
return False
|
|
||||||
progress_update(context, factor, text)
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
volume, meta = parse_volumetric_data(filepath=self.filepath,
|
|
||||||
series_id=self.series_id,
|
|
||||||
progress_callback=progress_callback)
|
|
||||||
except CancelledByUser:
|
|
||||||
return
|
|
||||||
except RuntimeError:
|
|
||||||
self.has_error = True
|
|
||||||
return
|
|
||||||
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
self.meta = meta
|
|
||||||
|
|
||||||
# Init cancel flag
|
|
||||||
self.is_cancelled = False
|
|
||||||
self.has_error = False
|
|
||||||
|
|
||||||
# Create the thread
|
|
||||||
self.thread = threading.Thread(target=parse_volumetric_data_func,
|
|
||||||
args=(self, context, lambda: self.is_cancelled))
|
|
||||||
|
|
||||||
# Start the thread
|
|
||||||
self.thread.start()
|
|
||||||
# Add a timmer for modal
|
|
||||||
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
|
||||||
window=context.window)
|
|
||||||
# Append progress bar to status bar
|
|
||||||
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
|
||||||
|
|
||||||
# Start modal handler
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def modal(self, context, event):
|
|
||||||
# Check if user press 'ESC'
|
|
||||||
if event.type == 'ESC':
|
|
||||||
self.is_cancelled = True
|
|
||||||
progress_update(context, 0, "Canceling...")
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
# Check if is the timer time
|
|
||||||
if event.type != 'TIMER':
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
# Force update status bar
|
|
||||||
bpy.context.workspace.status_text_set_internal(None)
|
|
||||||
|
|
||||||
# Check if thread is still running
|
|
||||||
if self.thread.is_alive():
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
# Release the thread
|
|
||||||
self.thread.join()
|
|
||||||
# Remove the timer
|
|
||||||
context.window_manager.event_timer_remove(self._timer)
|
|
||||||
# Remove the progress bar from status bar
|
|
||||||
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
|
||||||
|
|
||||||
# Check if thread is cancelled by user
|
|
||||||
if self.is_cancelled:
|
|
||||||
self.report({"WARNING"}, "Canncelled by user.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Check if thread is cancelled by user
|
|
||||||
if self.has_error:
|
|
||||||
self.report({"ERROR"}, "Fail to parse, something went wrong.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# If not canncelled...
|
|
||||||
for key, value in self.meta.items():
|
|
||||||
print(f"{key}: {value}")
|
|
||||||
|
|
||||||
orig_shape = self.meta['shape']
|
|
||||||
orig_spacing = self.meta['spacing']
|
|
||||||
min_size = min(orig_spacing[0],
|
|
||||||
orig_spacing[1], orig_spacing[2])
|
|
||||||
bioxel_size = max(min_size, 1.0)
|
|
||||||
|
|
||||||
layer_shape = get_layer_shape(1, orig_shape, orig_spacing)
|
|
||||||
layer_size = get_layer_size(layer_shape,
|
|
||||||
bioxel_size)
|
|
||||||
log10 = math.floor(math.log10(max(*layer_size)))
|
|
||||||
log10 = max(1,log10)
|
|
||||||
log10 = min(3,log10)
|
|
||||||
scene_scale = math.pow(10, -log10)
|
|
||||||
|
|
||||||
if self.container:
|
|
||||||
container = bpy.data.objects[self.container]
|
|
||||||
container_name = container.name
|
|
||||||
else:
|
|
||||||
container_name = self.meta['name']
|
|
||||||
|
|
||||||
bpy.ops.bioxelnodes.import_volumetric_data_dialog(
|
|
||||||
'INVOKE_DEFAULT',
|
|
||||||
filepath=self.filepath,
|
|
||||||
container_name=container_name,
|
|
||||||
layer_name=self.meta['description'],
|
|
||||||
orig_shape=orig_shape,
|
|
||||||
orig_spacing=orig_spacing,
|
|
||||||
bioxel_size=bioxel_size,
|
|
||||||
series_id=self.series_id or "",
|
|
||||||
frame_count=self.meta['frame_count'],
|
|
||||||
channel_count=self.meta['channel_count'],
|
|
||||||
container=self.container,
|
|
||||||
read_as=self.read_as,
|
|
||||||
scene_scale=scene_scale
|
|
||||||
)
|
|
||||||
|
|
||||||
self.report({"INFO"}, "Successfully Readed.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
if not self.filepath and not self.directory:
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
ext = get_ext(self.filepath)
|
|
||||||
|
|
||||||
# Series Selection
|
|
||||||
if ext in DICOM_EXTS:
|
|
||||||
dir_path = Path(self.filepath).parent
|
|
||||||
reader = sitk.ImageSeriesReader()
|
|
||||||
reader.MetaDataDictionaryArrayUpdateOn()
|
|
||||||
reader.LoadPrivateTagsOn()
|
|
||||||
|
|
||||||
series_ids = reader.GetGDCMSeriesIDs(str(dir_path))
|
|
||||||
|
|
||||||
for series_id in series_ids:
|
|
||||||
series_files = reader.GetGDCMSeriesFileNames(
|
|
||||||
str(dir_path), series_id)
|
|
||||||
single = sitk.ImageFileReader()
|
|
||||||
single.SetFileName(series_files[0])
|
|
||||||
single.LoadPrivateTagsOn()
|
|
||||||
single.ReadImageInformation()
|
|
||||||
|
|
||||||
def get_meta(key):
|
|
||||||
try:
|
|
||||||
stirng = single.GetMetaData(key).removesuffix(" ")
|
|
||||||
if stirng in ["No study description",
|
|
||||||
"No series description"]:
|
|
||||||
return "Unknown"
|
|
||||||
else:
|
|
||||||
return stirng
|
|
||||||
except:
|
|
||||||
return "Unknown"
|
|
||||||
|
|
||||||
study_description = get_meta("0008|1030")
|
|
||||||
series_description = get_meta("0008|103e")
|
|
||||||
series_modality = get_meta("0008|0060")
|
|
||||||
size_x = get_meta("0028|0011")
|
|
||||||
size_y = get_meta("0028|0010")
|
|
||||||
count = get_meta("0020|0013")
|
|
||||||
|
|
||||||
# some series image count = 0 ????
|
|
||||||
if int(count) == 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
series_item = self.series_ids.add()
|
|
||||||
series_item.id = series_id
|
|
||||||
series_item.label = "{:<20} {:>1}".format(f"{study_description}>{series_description}({series_modality})",
|
|
||||||
f"({size_x}x{size_y})x{count}")
|
|
||||||
|
|
||||||
if len(series_ids) > 1:
|
|
||||||
context.window_manager.invoke_props_dialog(self,
|
|
||||||
width=400,
|
|
||||||
title="Which series to import?")
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
else:
|
|
||||||
self.series_id = series_ids[0]
|
|
||||||
|
|
||||||
self.execute(context)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.label(
|
|
||||||
text='Detect multi-series in DICOM, pick one')
|
|
||||||
layout.prop(self, "series_id")
|
|
||||||
|
|
||||||
|
|
||||||
class ImportVolumetricDataDialog(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.import_volumetric_data_dialog"
|
|
||||||
bl_label = "Import Volumetric Data"
|
|
||||||
bl_description = "Import Volumetric Data as Layer"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
layers = []
|
|
||||||
thread = None
|
|
||||||
_timer = None
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
||||||
|
|
||||||
container_name: bpy.props.StringProperty(
|
|
||||||
name="Container Name") # type: ignore
|
|
||||||
|
|
||||||
layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore
|
|
||||||
|
|
||||||
series_id: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
container: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
frame_count: bpy.props.IntProperty() # type: ignore
|
|
||||||
|
|
||||||
channel_count: bpy.props.IntProperty() # type: ignore
|
|
||||||
|
|
||||||
read_as: bpy.props.EnumProperty(name="Read as",
|
|
||||||
default="scalar",
|
|
||||||
items=[("scalar", "Scalar", ""),
|
|
||||||
("label", "Labels", "")]) # type: ignore
|
|
||||||
|
|
||||||
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
|
|
||||||
soft_min=0.1, soft_max=10.0,
|
|
||||||
min=0.1, max=1e2,
|
|
||||||
default=1) # type: ignore
|
|
||||||
|
|
||||||
orig_spacing: bpy.props.FloatVectorProperty(name="Original Spacing",
|
|
||||||
default=(1, 1, 1)) # type: ignore
|
|
||||||
|
|
||||||
orig_shape: bpy.props.IntVectorProperty(name="Original Shape",
|
|
||||||
default=(100, 100, 100)) # type: ignore
|
|
||||||
|
|
||||||
scene_scale: bpy.props.FloatProperty(name="Scene Scale (Bioxel Unit pre Blender Unit)",
|
|
||||||
soft_min=0.0001, soft_max=10.0,
|
|
||||||
min=1e-6, max=1e6,
|
|
||||||
default=0.01) # type: ignore
|
|
||||||
|
|
||||||
as_time_sequence: bpy.props.BoolProperty(name="As Time Sequence",
|
|
||||||
default=False) # type: ignore
|
|
||||||
|
|
||||||
split_channels: bpy.props.BoolProperty(name="Split Channels",
|
|
||||||
default=False) # type: ignore
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
def import_volumetric_data_func(self, context, cancel):
|
|
||||||
progress_update(context, 0.0, "Parsing Volumetirc Data...")
|
|
||||||
|
|
||||||
def progress_callback(factor, text):
|
|
||||||
if cancel():
|
|
||||||
return False
|
|
||||||
progress_update(context, factor*0.2, text)
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
volume, meta = parse_volumetric_data(filepath=self.filepath,
|
|
||||||
series_id=self.series_id,
|
|
||||||
progress_callback=progress_callback)
|
|
||||||
except CancelledByUser:
|
|
||||||
return
|
|
||||||
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
layer_shape = get_layer_shape(self.bioxel_size,
|
|
||||||
meta['shape'],
|
|
||||||
self.orig_spacing)
|
|
||||||
layer_dtype = volume.dtype.num
|
|
||||||
|
|
||||||
# After sitk.DICOMOrient(), origin and direction will also orient base on LPS
|
|
||||||
# so we need to convert them into RAS
|
|
||||||
mat_lps2ras = axis_conversion(from_forward='-Z',
|
|
||||||
from_up='-Y',
|
|
||||||
to_forward='-Z',
|
|
||||||
to_up='Y').to_4x4()
|
|
||||||
|
|
||||||
mat_location = mathutils.Matrix.Translation(
|
|
||||||
mathutils.Vector(meta['origin'])
|
|
||||||
)
|
|
||||||
|
|
||||||
mat_rotation = mathutils.Matrix(
|
|
||||||
np.array(meta['direction']).reshape((3, 3))
|
|
||||||
).to_4x4()
|
|
||||||
|
|
||||||
mat_scale = mathutils.Matrix.Scale(self.bioxel_size,
|
|
||||||
4)
|
|
||||||
|
|
||||||
layer_transfrom = mat_lps2ras @ mat_location @ mat_rotation @ mat_scale \
|
|
||||||
if meta['is_oriented'] else mat_location @ mat_rotation @ mat_scale
|
|
||||||
|
|
||||||
def convert_to_vdb(volume, layer_shape, layer_type, progress_callback=None):
|
|
||||||
if self.as_time_sequence:
|
|
||||||
grids_sequence = []
|
|
||||||
for f in range(volume.shape[0]):
|
|
||||||
if cancel():
|
|
||||||
raise CancelledByUser
|
|
||||||
|
|
||||||
print(f"Processing Frame {f+1}...")
|
|
||||||
if progress_callback:
|
|
||||||
progress_callback(f, volume.shape[0])
|
|
||||||
frame = ski.resize(volume[f, :, :, :],
|
|
||||||
layer_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=volume.dtype.kind != "b")
|
|
||||||
|
|
||||||
grid = vdb.FloatGrid()
|
|
||||||
frame = frame.copy().astype(np.float32)
|
|
||||||
grid.copyFromArray(frame)
|
|
||||||
grid.transform = vdb.createLinearTransform(
|
|
||||||
layer_transfrom.transposed())
|
|
||||||
grid.name = layer_type
|
|
||||||
grids_sequence.append([grid])
|
|
||||||
|
|
||||||
print(f"Saving the Cache...")
|
|
||||||
vdb_paths = save_vdbs(grids_sequence, context)
|
|
||||||
|
|
||||||
else:
|
|
||||||
if cancel():
|
|
||||||
raise CancelledByUser
|
|
||||||
print(f"Processing the Data...")
|
|
||||||
volume = ski.resize(volume,
|
|
||||||
layer_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=volume.dtype.kind != "b")
|
|
||||||
|
|
||||||
grid = vdb.FloatGrid()
|
|
||||||
volume = volume.copy().astype(np.float32)
|
|
||||||
grid.copyFromArray(volume)
|
|
||||||
grid.transform = vdb.createLinearTransform(
|
|
||||||
layer_transfrom.transposed())
|
|
||||||
grid.name = layer_type
|
|
||||||
|
|
||||||
print(f"Saving the Cache...")
|
|
||||||
vdb_paths = [save_vdb([grid], context)]
|
|
||||||
|
|
||||||
return vdb_paths
|
|
||||||
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
# change shape as sequence or not
|
|
||||||
if self.as_time_sequence:
|
|
||||||
if volume.shape[0] == 1:
|
|
||||||
# channel as frame
|
|
||||||
volume = volume.transpose(4, 1, 2, 3, 0)
|
|
||||||
|
|
||||||
else:
|
|
||||||
volume = volume[0, :, :, :, :]
|
|
||||||
|
|
||||||
layers = []
|
|
||||||
|
|
||||||
if self.read_as == "label":
|
|
||||||
layer_name = self.layer_name or "Label"
|
|
||||||
volume = np.amax(volume, -1)
|
|
||||||
volume = volume.astype(int)
|
|
||||||
orig_max = int(np.max(volume))
|
|
||||||
orig_min = int(np.min(volume))
|
|
||||||
progress_step = 0.7/orig_max
|
|
||||||
|
|
||||||
for i in range(orig_max):
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
layer_name_i = f"{layer_name}_{i+1}"
|
|
||||||
progress = 0.2+i*progress_step
|
|
||||||
progress_update(context, progress,
|
|
||||||
f"Processing {layer_name_i}...")
|
|
||||||
|
|
||||||
def progress_callback(frame, total):
|
|
||||||
sub_progress_step = progress_step/total
|
|
||||||
sub_progress = progress + frame * sub_progress_step
|
|
||||||
progress_update(context, sub_progress,
|
|
||||||
f"Processing {layer_name_i} Frame {frame+1}...")
|
|
||||||
|
|
||||||
label = volume == np.full_like(volume, i+1)
|
|
||||||
try:
|
|
||||||
filepaths = convert_to_vdb(volume=label,
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="label",
|
|
||||||
progress_callback=progress_callback)
|
|
||||||
except CancelledByUser:
|
|
||||||
return
|
|
||||||
|
|
||||||
layers.append({"name": layer_name_i,
|
|
||||||
"filepaths": filepaths,
|
|
||||||
"type": "label",
|
|
||||||
"shape": layer_shape,
|
|
||||||
"transfrom": layer_transfrom,
|
|
||||||
"dtype": layer_dtype,
|
|
||||||
"node_type": "BioxelNodes_MaskByLabel",
|
|
||||||
"scalar_offset": 0,
|
|
||||||
"orig_min": 0,
|
|
||||||
"orig_max": 1})
|
|
||||||
|
|
||||||
elif self.read_as == "scalar":
|
|
||||||
layer_name = self.layer_name or "Scalar"
|
|
||||||
# SHOULD NOT change any value!
|
|
||||||
volume = volume.astype(np.float32)
|
|
||||||
|
|
||||||
if self.split_channels:
|
|
||||||
progress_step = 0.7/volume.shape[-1]
|
|
||||||
for i in range(volume.shape[-1]):
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
layer_name_i = f"{layer_name}_{i+1}"
|
|
||||||
progress = 0.2 + i*progress_step
|
|
||||||
|
|
||||||
progress_update(context, progress,
|
|
||||||
f"Processing {layer_name_i}...")
|
|
||||||
|
|
||||||
def progress_callback(frame, total):
|
|
||||||
sub_progress_step = progress_step/total
|
|
||||||
sub_progress = progress + frame * sub_progress_step
|
|
||||||
progress_update(context, sub_progress,
|
|
||||||
f"Processing {layer_name_i} Frame {frame+1}...")
|
|
||||||
|
|
||||||
scalar = volume[:, :, :, :, i] \
|
|
||||||
if self.as_time_sequence else volume[:, :, :, i]
|
|
||||||
orig_max = float(np.max(scalar))
|
|
||||||
orig_min = float(np.min(scalar))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if orig_min < 0:
|
|
||||||
scalar_offset = -orig_min
|
|
||||||
scalar = scalar + \
|
|
||||||
np.full_like(scalar, scalar_offset)
|
|
||||||
try:
|
|
||||||
filepaths = convert_to_vdb(volume=scalar,
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="scalar",
|
|
||||||
progress_callback=progress_callback)
|
|
||||||
except CancelledByUser:
|
|
||||||
return
|
|
||||||
|
|
||||||
layers.append({"name": layer_name_i,
|
|
||||||
"filepaths": filepaths,
|
|
||||||
"type": "scalar",
|
|
||||||
"shape": layer_shape,
|
|
||||||
"transfrom": layer_transfrom,
|
|
||||||
"dtype": layer_dtype,
|
|
||||||
"node_type": "BioxelNodes_MaskByThreshold",
|
|
||||||
"scalar_offset": scalar_offset,
|
|
||||||
"orig_min": orig_min,
|
|
||||||
"orig_max": orig_max})
|
|
||||||
|
|
||||||
else:
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
progress_update(context, 0.2,
|
|
||||||
f"Processing {layer_name}...")
|
|
||||||
volume = np.amax(volume, -1)
|
|
||||||
orig_max = float(np.max(volume))
|
|
||||||
orig_min = float(np.min(volume))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if orig_min < 0:
|
|
||||||
scalar_offset = -orig_min
|
|
||||||
volume = volume + np.full_like(volume, scalar_offset)
|
|
||||||
|
|
||||||
def progress_callback(frame, total):
|
|
||||||
sub_progress_step = 0.7/total
|
|
||||||
sub_progress = 0.2 + frame * sub_progress_step
|
|
||||||
progress_update(context, sub_progress,
|
|
||||||
f"Processing {layer_name} Frame {frame+1}...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
filepaths = convert_to_vdb(volume=volume,
|
|
||||||
layer_shape=layer_shape,
|
|
||||||
layer_type="scalar",
|
|
||||||
progress_callback=progress_callback)
|
|
||||||
except CancelledByUser:
|
|
||||||
return
|
|
||||||
|
|
||||||
layers.append({"name": layer_name,
|
|
||||||
"filepaths": filepaths,
|
|
||||||
"type": "scalar",
|
|
||||||
"shape": layer_shape,
|
|
||||||
"transfrom": layer_transfrom,
|
|
||||||
"dtype": layer_dtype,
|
|
||||||
"node_type": "BioxelNodes_MaskByThreshold",
|
|
||||||
"scalar_offset": scalar_offset,
|
|
||||||
"orig_min": orig_min,
|
|
||||||
"orig_max": orig_max})
|
|
||||||
|
|
||||||
if cancel():
|
|
||||||
return
|
|
||||||
|
|
||||||
self.layers = layers
|
|
||||||
self.is_first_import = len(get_all_layers()) == 0
|
|
||||||
progress_update(context, 0.9, "Creating Layers...")
|
|
||||||
|
|
||||||
self.is_cancelled = False
|
|
||||||
self.thread = threading.Thread(target=import_volumetric_data_func,
|
|
||||||
args=(self, context, lambda: self.is_cancelled))
|
|
||||||
|
|
||||||
self.thread.start()
|
|
||||||
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
|
||||||
window=context.window)
|
|
||||||
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
|
||||||
|
|
||||||
context.window_manager.modal_handler_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def modal(self, context, event):
|
|
||||||
if event.type == 'ESC':
|
|
||||||
self.is_cancelled = True
|
|
||||||
progress_update(context, 0, "Canceling...")
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
if event.type != 'TIMER':
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
bpy.context.workspace.status_text_set_internal(None)
|
|
||||||
if self.thread.is_alive():
|
|
||||||
return {'PASS_THROUGH'}
|
|
||||||
|
|
||||||
self.thread.join()
|
|
||||||
context.window_manager.event_timer_remove(self._timer)
|
|
||||||
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
|
||||||
|
|
||||||
if self.is_cancelled:
|
|
||||||
self.report({"WARNING"}, "Canncelled by user.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
# Wrapper a Container
|
|
||||||
if not self.container:
|
|
||||||
container_name = self.container_name or "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 = mathutils.Matrix.Scale(self.scene_scale,
|
|
||||||
4)
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(enter_editmode=False,
|
|
||||||
align='WORLD',
|
|
||||||
location=(0, 0, 0),
|
|
||||||
scale=(1, 1, 1))
|
|
||||||
|
|
||||||
container = bpy.context.active_object
|
|
||||||
|
|
||||||
bbox_verts = calc_bbox_verts((0, 0, 0), self.layers[0]['shape'])
|
|
||||||
for i, vert in enumerate(container.data.vertices):
|
|
||||||
vert.co = self.layers[0]['transfrom'] @ mathutils.Vector(
|
|
||||||
bbox_verts[i])
|
|
||||||
|
|
||||||
container.matrix_world = mat_ras2blender @ mat_scene_scale
|
|
||||||
container.name = container_name
|
|
||||||
container.show_in_front = True
|
|
||||||
container.data.name = container_name
|
|
||||||
|
|
||||||
container['bioxel_container'] = True
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
container_node_group = container.modifiers[0].node_group
|
|
||||||
input_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupInput')[0]
|
|
||||||
container_node_group.links.remove(
|
|
||||||
input_node.outputs[0].links[0])
|
|
||||||
|
|
||||||
else:
|
|
||||||
container = bpy.data.objects[self.container]
|
|
||||||
container_node_group = container.modifiers[0].node_group
|
|
||||||
|
|
||||||
for i, layer_info in enumerate(self.layers):
|
|
||||||
# Read VDB
|
|
||||||
print(f"Loading the Cache of {layer_info['name']}...")
|
|
||||||
filepaths = layer_info["filepaths"]
|
|
||||||
if len(filepaths) > 0:
|
|
||||||
# Read VDB
|
|
||||||
files = [{"name": str(filepath.name), "name": str(filepath.name)}
|
|
||||||
for filepath in filepaths]
|
|
||||||
|
|
||||||
bpy.ops.object.volume_import(filepath=str(filepaths[0]), directory=str(filepaths[0].parent),
|
|
||||||
files=files, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
else:
|
|
||||||
bpy.ops.object.volume_import(filepath=str(filepaths[0]),
|
|
||||||
align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
|
|
||||||
layer = bpy.context.active_object
|
|
||||||
layer.data.sequence_mode = 'REPEAT'
|
|
||||||
|
|
||||||
# Set props to VDB object
|
|
||||||
layer.name = f"{container.name}_{layer_info['name']}"
|
|
||||||
layer.data.name = f"{container.name}_{layer_info['name']}"
|
|
||||||
|
|
||||||
lock_transform(layer)
|
|
||||||
hide_in_ray(layer)
|
|
||||||
layer.hide_select = True
|
|
||||||
layer.hide_render = True
|
|
||||||
layer.hide_viewport = True
|
|
||||||
# layer.data.display.use_slice = True
|
|
||||||
# layer.data.display.density = 1e-05
|
|
||||||
|
|
||||||
layer['bioxel_layer'] = True
|
|
||||||
layer['bioxel_layer_type'] = layer_info['type']
|
|
||||||
layer.parent = container
|
|
||||||
|
|
||||||
for collection in layer.users_collection:
|
|
||||||
collection.objects.unlink(layer)
|
|
||||||
|
|
||||||
for collection in container.users_collection:
|
|
||||||
collection.objects.link(layer)
|
|
||||||
|
|
||||||
print(f"Creating Node for {layer_info['name']}...")
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
node_group = layer.modifiers[0].node_group
|
|
||||||
|
|
||||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
|
||||||
output_node = get_nodes_by_type(
|
|
||||||
node_group, 'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
to_layer_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes__ConvertToLayer")
|
|
||||||
|
|
||||||
node_group.links.new(input_node.outputs[0],
|
|
||||||
to_layer_node.inputs[0])
|
|
||||||
node_group.links.new(to_layer_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
|
|
||||||
# TODO: change to transform when 4.2?
|
|
||||||
loc, rot, sca = layer_info['transfrom'].decompose()
|
|
||||||
layer_origin = tuple(loc)
|
|
||||||
layer_rotation = tuple(rot.to_euler())
|
|
||||||
|
|
||||||
# for compatibility to old vdb
|
|
||||||
# to_layer_node.inputs['Not Transfromed'].default_value = True
|
|
||||||
to_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
|
||||||
200000000)
|
|
||||||
to_layer_node.inputs['Bioxel Size'].default_value = self.bioxel_size
|
|
||||||
to_layer_node.inputs['Data Type'].default_value = layer_info['dtype']
|
|
||||||
to_layer_node.inputs['Shape'].default_value = layer_info['shape']
|
|
||||||
to_layer_node.inputs['Origin'].default_value = layer_origin
|
|
||||||
to_layer_node.inputs['Rotation'].default_value = layer_rotation
|
|
||||||
to_layer_node.inputs['Scalar Offset'].default_value = layer_info['scalar_offset']
|
|
||||||
to_layer_node.inputs['Scalar Min'].default_value = layer_info['orig_min']
|
|
||||||
to_layer_node.inputs['Scalar Max'].default_value = layer_info['orig_max']
|
|
||||||
|
|
||||||
move_node_between_nodes(
|
|
||||||
to_layer_node, [input_node, output_node])
|
|
||||||
|
|
||||||
mask_node = custom_nodes.add_node(container_node_group,
|
|
||||||
layer_info['node_type'])
|
|
||||||
mask_node.label = layer_info['name']
|
|
||||||
mask_node.inputs[0].default_value = layer
|
|
||||||
|
|
||||||
# Connect to output if no output linked
|
|
||||||
output_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupOutput')[0]
|
|
||||||
if len(output_node.inputs[0].links) == 0:
|
|
||||||
container_node_group.links.new(mask_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
|
||||||
else:
|
|
||||||
move_node_to_node(mask_node, output_node, (0, -100 * (i+1)))
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
# Change render setting for better result
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
if preferences.do_change_render_setting and self.is_first_import:
|
|
||||||
bpy.context.scene.render.engine = 'CYCLES'
|
|
||||||
try:
|
|
||||||
bpy.context.scene.cycles.shading_system = True
|
|
||||||
bpy.context.scene.cycles.volume_bounces = 12
|
|
||||||
bpy.context.scene.cycles.transparent_max_bounces = 16
|
|
||||||
bpy.context.scene.cycles.volume_preview_step_rate = 10
|
|
||||||
bpy.context.scene.cycles.volume_step_rate = 10
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
bpy.context.scene.eevee.use_taa_reprojection = False
|
|
||||||
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
|
||||||
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
|
||||||
bpy.context.scene.eevee.volumetric_samples = 256
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if bpy.app.version >= (4, 2, 0):
|
|
||||||
try:
|
|
||||||
bpy.context.scene.eevee.use_volumetric_shadows = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.report({"INFO"}, "Successfully Imported")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
if self.read_as == "label":
|
|
||||||
volume_dtype = "Label"
|
|
||||||
elif self.read_as == "scalar":
|
|
||||||
volume_dtype = "Scalar"
|
|
||||||
title = f"As {volume_dtype} Opitons (Add to Container: {self.container})" \
|
|
||||||
if self.container != "" else f"As {volume_dtype} Options (Init a Container)"
|
|
||||||
context.window_manager.invoke_props_dialog(self,
|
|
||||||
width=500,
|
|
||||||
title=title)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layer_shape = get_layer_shape(self.bioxel_size,
|
|
||||||
self.orig_shape,
|
|
||||||
self.orig_spacing)
|
|
||||||
|
|
||||||
orig_shape = tuple(self.orig_shape)
|
|
||||||
|
|
||||||
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
|
||||||
layer_shape_text = f"Shape from {str(orig_shape)} to {str(layer_shape)}"
|
|
||||||
if bioxel_count > 100000000:
|
|
||||||
layer_shape_text += "**TOO LARGE!**"
|
|
||||||
|
|
||||||
layout = self.layout
|
|
||||||
if self.container == "":
|
|
||||||
layout.prop(self, "container_name")
|
|
||||||
layout.prop(self, "layer_name")
|
|
||||||
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "bioxel_size")
|
|
||||||
row = panel.row()
|
|
||||||
row.prop(self, "orig_spacing")
|
|
||||||
panel.label(text=layer_shape_text)
|
|
||||||
|
|
||||||
panel = layout.box()
|
|
||||||
if self.as_time_sequence and self.frame_count == 1:
|
|
||||||
channel_count = 1
|
|
||||||
frame_count = self.channel_count
|
|
||||||
else:
|
|
||||||
channel_count = self.channel_count
|
|
||||||
frame_count = self.frame_count
|
|
||||||
|
|
||||||
import_channel = channel_count if self.split_channels or channel_count == 1 else "combined"
|
|
||||||
import_frame = frame_count if self.as_time_sequence else "1"
|
|
||||||
panel.prop(self, "as_time_sequence",
|
|
||||||
text=f"As Time Sequence (get {frame_count} frames, import {import_frame} frames)")
|
|
||||||
if self.read_as == "scalar":
|
|
||||||
panel.prop(self, "split_channels",
|
|
||||||
text=f"Split Channels (get {channel_count} channels, import {import_channel} channels)")
|
|
||||||
|
|
||||||
if self.container == "":
|
|
||||||
layer_size = get_layer_size(layer_shape,
|
|
||||||
self.bioxel_size,
|
|
||||||
self.scene_scale)
|
|
||||||
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "scene_scale")
|
|
||||||
panel.label(text=layer_size_text)
|
|
||||||
|
|
||||||
|
|
||||||
class ExportVolumetricData(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.export_volumetric_data"
|
|
||||||
bl_label = "Export Layer as VDB"
|
|
||||||
bl_description = "Export Layer as VDB"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
filepath: bpy.props.StringProperty(
|
|
||||||
subtype="FILE_PATH"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
filename_ext = ".vdb"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
return True if layer else False
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.fileselect_add(self)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
filepath = f"{self.filepath.split('.')[0]}.vdb"
|
|
||||||
# "//"
|
|
||||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
|
|
||||||
output_path: Path = Path(filepath).resolve()
|
|
||||||
source_path: Path = Path(source_dir).resolve()
|
|
||||||
# print('output_path', output_path)
|
|
||||||
# print('source_path', source_path)
|
|
||||||
shutil.copy(source_path, output_path)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully exported to {output_path}")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# TODO: WIP
|
|
||||||
import bpy
|
|
||||||
import bmesh
|
|
||||||
|
|
||||||
|
|
||||||
def create_points_obj(points, name="points"):
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)
|
|
||||||
)
|
|
||||||
obj = bpy.context.active_object
|
|
||||||
mesh = obj.data
|
|
||||||
|
|
||||||
bm = bmesh.new()
|
|
||||||
value_layer = bm.verts.layers.float.new('value')
|
|
||||||
|
|
||||||
for point in points:
|
|
||||||
pos = point['pos']
|
|
||||||
value = point['value']
|
|
||||||
vert = bm.verts.new(pos) # add a new vert
|
|
||||||
vert[value_layer] = value
|
|
||||||
|
|
||||||
# make the bmesh the object's mesh
|
|
||||||
bm.to_mesh(mesh)
|
|
||||||
bm.free() # always do this when finished
|
|
||||||
return obj
|
|
||||||
@@ -1,150 +1,160 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from .utils import get_container_from_selection
|
from .constants import MENU_ITEMS, NODE_LIB_FILENAME
|
||||||
from .operators import (AddPieCutter, AddPlaneCutter, AddCylinderCutter, AddCubeCutter, AddSphereCutter, CombineLabels,
|
from .node_menu import NodeMenu
|
||||||
ConvertToMesh, InvertScalar, FillByLabel, FillByThreshold, FillByRange, PickBboxWire, PickMesh, PickVolume)
|
|
||||||
from .io import ExportVolumetricData, ImportAsLabelLayer, ImportAsScalarLayer
|
from .bioxelutils.common import (get_container_obj,
|
||||||
from .save import CleanAllCaches, ReLinkNodes, SaveLayers, SaveStagedData
|
get_container_layer_objs, get_layer_label,
|
||||||
|
get_layer_prop_value)
|
||||||
|
from .operators.layer import (FetchLayer, RelocateLayer, RetimeLayer, RenameLayer,
|
||||||
|
RemoveSelectedLayers, SaveSelectedLayersCache,
|
||||||
|
ResampleLayer, SignScalar, CombineLabels,
|
||||||
|
FillByLabel, FillByThreshold, FillByRange)
|
||||||
|
from .operators.container import (AddLocator, AddSlicer, ExtractShapeWire,
|
||||||
|
SaveContainerLayersCache, RemoveContainerMissingLayers,
|
||||||
|
SaveContainer, LoadContainer,
|
||||||
|
AddPieCutter, AddPlaneCutter,
|
||||||
|
AddCylinderCutter, AddCubeCutter, AddSphereCutter,
|
||||||
|
ExtractBboxWire, ExtractSurface, ExtractVolume, ContainerProps)
|
||||||
|
|
||||||
|
from .operators.io import (ImportAsLabel, ImportAsScalar, ImportAsColor)
|
||||||
|
from .operators.misc import (CleanTemp,
|
||||||
|
ReLinkNodeLib, RemoveAllMissingLayers, RenderSettingPreset, SaveAllLayersCache, SaveNodeLib, SliceViewer)
|
||||||
|
|
||||||
|
|
||||||
class PickFromContainerMenu(bpy.types.Menu):
|
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_idname = "BIOXELNODES_MT_PICK"
|
||||||
bl_label = "Pick from Container"
|
bl_label = "Extract from Container"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator(PickMesh.bl_idname)
|
layout.operator(ExtractSurface.bl_idname)
|
||||||
layout.operator(PickVolume.bl_idname)
|
layout.operator(ExtractVolume.bl_idname)
|
||||||
layout.operator(PickBboxWire.bl_idname)
|
layout.operator(ExtractShapeWire.bl_idname)
|
||||||
|
layout.operator(ExtractBboxWire.bl_idname)
|
||||||
|
|
||||||
class ModifyLayerMenu(bpy.types.Menu):
|
|
||||||
bl_idname = "BIOXELNODES_MT_MODIFY_LAYERS"
|
|
||||||
bl_label = "Modify Layer"
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.operator(InvertScalar.bl_idname)
|
|
||||||
layout.operator(FillByThreshold.bl_idname)
|
|
||||||
layout.operator(FillByRange.bl_idname)
|
|
||||||
layout.operator(FillByLabel.bl_idname)
|
|
||||||
layout.operator(CombineLabels.bl_idname)
|
|
||||||
|
|
||||||
|
|
||||||
class AddCutterMenu(bpy.types.Menu):
|
class AddCutterMenu(bpy.types.Menu):
|
||||||
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
bl_idname = "BIOXELNODES_MT_CUTTERS"
|
||||||
bl_label = "Add a Cutter to Container"
|
bl_label = "Add a Cutter"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator(AddPlaneCutter.bl_idname, text="Plane Cutter")
|
layout.operator(AddPlaneCutter.bl_idname,
|
||||||
layout.operator(AddCylinderCutter.bl_idname, text="Cylinder Cutter")
|
icon=AddPlaneCutter.bl_icon, text="Plane Cutter")
|
||||||
layout.operator(AddCubeCutter.bl_idname, text="Cube Cutter")
|
layout.operator(AddCylinderCutter.bl_idname,
|
||||||
layout.operator(AddSphereCutter.bl_idname, text="Sphere Cutter")
|
icon=AddCylinderCutter.bl_icon, text="Cylinder Cutter")
|
||||||
layout.operator(AddPieCutter.bl_idname, text="Pie 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")
|
||||||
|
|
||||||
|
|
||||||
class ImportLayerMenu(bpy.types.Menu):
|
class ImportLayerMenu(bpy.types.Menu):
|
||||||
bl_idname = "BIOXELNODES_MT_LAYERS"
|
bl_idname = "BIOXELNODES_MT_IMPORTLAYER"
|
||||||
bl_label = "Import Volumetric Data"
|
bl_label = "Import Volumetric Data (Init)"
|
||||||
|
bl_icon = "FILE_NEW"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.operator(ImportAsScalarLayer.bl_idname, text="as Scalar")
|
layout.operator(ImportAsScalar.bl_idname,
|
||||||
layout.operator(ImportAsLabelLayer.bl_idname, text="as Label")
|
text="as Scalar")
|
||||||
|
layout.operator(ImportAsLabel.bl_idname,
|
||||||
|
text="as Label")
|
||||||
|
layout.operator(ImportAsColor.bl_idname,
|
||||||
|
text="as Color")
|
||||||
|
|
||||||
|
|
||||||
class BioxelNodesView3DMenu(bpy.types.Menu):
|
class AddLayerMenu(bpy.types.Menu):
|
||||||
bl_idname = "BIOXELNODES_MT_VIEW3D"
|
bl_idname = "BIOXELNODES_MT_ADDLAYER"
|
||||||
bl_label = "Bioxel Nodes"
|
bl_label = "Import Volumetric Data (Add)"
|
||||||
|
bl_icon = "FILE_NEW"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
layout.operator(ImportAsScalar.bl_idname,
|
||||||
is_selected = len(containers) > 0
|
text="as Scalar")
|
||||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
layout.operator(ImportAsLabel.bl_idname,
|
||||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
text="as Label")
|
||||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
layout.operator(ImportAsColor.bl_idname,
|
||||||
layout.operator(ImportAsLabelLayer.bl_idname,
|
text="as Color")
|
||||||
text=ImportAsLabelLayer.bl_label+" (Add to)"
|
|
||||||
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(AddPlaneCutter.bl_idname)
|
|
||||||
layout.operator(AddCylinderCutter.bl_idname)
|
|
||||||
layout.operator(AddCubeCutter.bl_idname)
|
|
||||||
layout.operator(AddSphereCutter.bl_idname)
|
|
||||||
layout.operator(AddPieCutter.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(PickMesh.bl_idname)
|
|
||||||
layout.operator(PickVolume.bl_idname)
|
|
||||||
layout.operator(PickBboxWire.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(SaveLayers.bl_idname)
|
|
||||||
|
|
||||||
|
|
||||||
class BioxelNodesOutlinerMenu(bpy.types.Menu):
|
class ModifyLayerMenu(bpy.types.Menu):
|
||||||
bl_idname = "BIOXELNODES_MT_OUTLINER"
|
bl_idname = "BIOXELNODES_MT_MODIFYLAYER"
|
||||||
bl_label = "Bioxel Nodes"
|
bl_label = "Modify Layer"
|
||||||
|
bl_icon = "FILE_NEW"
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
layout.operator(SignScalar.bl_idname,
|
||||||
is_selected = len(containers) > 0
|
icon=SignScalar.bl_icon)
|
||||||
layout.operator(ImportAsScalarLayer.bl_idname,
|
layout.operator(FillByThreshold.bl_idname,
|
||||||
text=ImportAsScalarLayer.bl_label+" (Add to)"
|
icon=FillByThreshold.bl_icon)
|
||||||
if is_selected else ImportAsScalarLayer.bl_label+" (Init)")
|
layout.operator(FillByRange.bl_idname,
|
||||||
layout.operator(ImportAsLabelLayer.bl_idname,
|
icon=FillByRange.bl_icon)
|
||||||
text=ImportAsLabelLayer.bl_label+" (Add to)"
|
layout.operator(FillByLabel.bl_idname,
|
||||||
if is_selected else ImportAsLabelLayer.bl_label+" (Init)")
|
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
|
||||||
|
versions = [{"label": "v0.3.x", "filename": "BioxelNodes_v0.3.x"},
|
||||||
|
{"label": "v0.2.x", "filename": "BioxelNodes_v0.2.x"},
|
||||||
|
{"label": "v0.1.x", "filename": "BioxelNodes_v0.1.x"}]
|
||||||
|
|
||||||
|
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||||
|
text="Current Version")
|
||||||
|
op.node_lib_filename = NODE_LIB_FILENAME
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(AddPlaneCutter.bl_idname)
|
for version in versions:
|
||||||
layout.operator(AddCylinderCutter.bl_idname)
|
op = layout.operator(ReLinkNodeLib.bl_idname,
|
||||||
layout.operator(AddCubeCutter.bl_idname)
|
text=version["label"])
|
||||||
layout.operator(AddSphereCutter.bl_idname)
|
op.node_lib_filename = version["filename"]
|
||||||
layout.operator(AddPieCutter.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(PickMesh.bl_idname)
|
|
||||||
layout.operator(PickVolume.bl_idname)
|
|
||||||
layout.operator(PickBboxWire.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(SaveLayers.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(InvertScalar.bl_idname)
|
|
||||||
layout.operator(FillByThreshold.bl_idname)
|
|
||||||
layout.operator(FillByRange.bl_idname)
|
|
||||||
layout.operator(FillByLabel.bl_idname)
|
|
||||||
layout.operator(CombineLabels.bl_idname)
|
|
||||||
layout.separator()
|
|
||||||
layout.operator(ExportVolumetricData.bl_idname)
|
|
||||||
|
|
||||||
|
|
||||||
def TOPBAR_FILE_IMPORT(self, context):
|
class RenderSettingMenu(bpy.types.Menu):
|
||||||
layout = self.layout
|
bl_idname = "BIOXELNODES_MT_RENDER"
|
||||||
containers = get_container_from_selection()
|
bl_label = "Render Setting Presets"
|
||||||
is_selected = len(containers) > 0
|
|
||||||
|
|
||||||
layout.separator()
|
def draw(self, context):
|
||||||
layout.menu(ImportLayerMenu.bl_idname, text="Volumetric Data as Bioxel (Add to)"
|
layout = self.layout
|
||||||
if is_selected else "Volumetric Data as Bioxel (Init)")
|
for k, v in RenderSettingPreset.PRESETS.items():
|
||||||
|
op = layout.operator(RenderSettingPreset.bl_idname,
|
||||||
|
text=v)
|
||||||
|
op.preset = k
|
||||||
|
|
||||||
|
|
||||||
def TOPBAR_FILE_EXPORT(self, context):
|
class DangerZoneMenu(bpy.types.Menu):
|
||||||
layout = self.layout
|
bl_idname = "BIOXELNODES_MT_DANGER"
|
||||||
layout.separator()
|
bl_label = "Danger Zone"
|
||||||
layout.operator(ExportVolumetricData.bl_idname,
|
|
||||||
text="Bioxel Layer (.vdb)")
|
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
def VIEW3D_OBJECT(self, context):
|
layout = self.layout
|
||||||
layout = self.layout
|
layout.operator(CleanTemp.bl_idname)
|
||||||
layout.separator()
|
|
||||||
layout.menu(BioxelNodesView3DMenu.bl_idname, icon="FILE_VOLUME")
|
|
||||||
|
|
||||||
|
|
||||||
def OUTLINER_OBJECT(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
layout.separator()
|
|
||||||
layout.menu(BioxelNodesOutlinerMenu.bl_idname, icon="FILE_VOLUME")
|
|
||||||
|
|
||||||
|
|
||||||
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
class BioxelNodesTopbarMenu(bpy.types.Menu):
|
||||||
@@ -153,37 +163,219 @@ class BioxelNodesTopbarMenu(bpy.types.Menu):
|
|||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
containers = get_container_from_selection()
|
|
||||||
is_selected = len(containers) > 0
|
|
||||||
|
|
||||||
layout.menu(ImportLayerMenu.bl_idname, text=ImportLayerMenu.bl_label+" (Add to)"
|
layout.menu(ImportLayerMenu.bl_idname,
|
||||||
if is_selected else ImportLayerMenu.bl_label+" (Init)")
|
icon=ImportLayerMenu.bl_icon)
|
||||||
|
layout.operator(LoadContainer.bl_idname)
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.menu(AddCutterMenu.bl_idname)
|
layout.operator(SaveAllLayersCache.bl_idname)
|
||||||
layout.menu(PickFromContainerMenu.bl_idname)
|
layout.operator(RemoveAllMissingLayers.bl_idname)
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(SaveStagedData.bl_idname)
|
layout.operator(SaveNodeLib.bl_idname)
|
||||||
layout.operator(ReLinkNodes.bl_idname)
|
layout.menu(ReLinkNodeLibMenu.bl_idname)
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
layout.operator(CleanAllCaches.bl_idname)
|
layout.menu(DangerZoneMenu.bl_idname)
|
||||||
|
|
||||||
|
layout.separator()
|
||||||
|
layout.menu(RenderSettingMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
def TOPBAR(self, context):
|
def TOPBAR(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
layout.menu(BioxelNodesTopbarMenu.bl_idname)
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
class NodeContextMenu(bpy.types.Menu):
|
||||||
|
bl_idname = "BIOXELNODES_MT_NODE_CONTEXT"
|
||||||
|
bl_label = "Bioxel Nodes"
|
||||||
|
bl_icon = "FILE_VOLUME"
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 NODE_CONTEXT(self, context):
|
||||||
|
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
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
layout.menu(NodeContextMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
|
def NODE_HEAD(self, context):
|
||||||
|
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
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
layout.separator()
|
||||||
|
layout.menu(NodeHeadMenu.bl_idname)
|
||||||
|
|
||||||
|
|
||||||
|
def NODE_PROP(self, context):
|
||||||
|
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,
|
||||||
|
icon=SliceViewer.bl_icon, text="")
|
||||||
|
|
||||||
|
|
||||||
|
node_menu = NodeMenu(
|
||||||
|
menu_items=MENU_ITEMS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add():
|
def add():
|
||||||
bpy.types.TOPBAR_MT_file_import.append(TOPBAR_FILE_IMPORT)
|
node_menu.register()
|
||||||
bpy.types.TOPBAR_MT_file_export.append(TOPBAR_FILE_EXPORT)
|
bpy.types.VIEW3D_HT_header.append(VIEW3D_TOPBAR)
|
||||||
bpy.types.OUTLINER_MT_object.append(OUTLINER_OBJECT)
|
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.append(VIEW3D_OBJECT)
|
|
||||||
bpy.types.TOPBAR_MT_editor_menus.append(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)
|
||||||
|
|
||||||
|
|
||||||
def remove():
|
def remove():
|
||||||
bpy.types.TOPBAR_MT_file_import.remove(TOPBAR_FILE_IMPORT)
|
bpy.types.VIEW3D_HT_header.remove(VIEW3D_TOPBAR)
|
||||||
bpy.types.TOPBAR_MT_file_export.remove(TOPBAR_FILE_EXPORT)
|
|
||||||
bpy.types.OUTLINER_MT_object.remove(OUTLINER_OBJECT)
|
|
||||||
bpy.types.VIEW3D_MT_object_context_menu.remove(VIEW3D_OBJECT)
|
|
||||||
bpy.types.TOPBAR_MT_editor_menus.remove(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()
|
||||||
|
|||||||
87
bioxelnodes/node_menu.py
Normal file
87
bioxelnodes/node_menu.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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,282 +0,0 @@
|
|||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from .customnodes import CustomNodes
|
|
||||||
import bpy
|
|
||||||
|
|
||||||
# def set_object_to_node_factory(object_type:str):
|
|
||||||
# if object_type == "plane":
|
|
||||||
# create_object = """
|
|
||||||
# bpy.ops.mesh.primitive_plane_add(size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
# """
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from ..utils import get_container
|
|
||||||
# container = get_container(bpy.context.active_object)
|
|
||||||
# if container:
|
|
||||||
# {create_object}
|
|
||||||
# object = bpy.context.active_object
|
|
||||||
# node.inputs[0].default_value = object
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any Bioxel Container.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
# def add_driver_to_node_factory(source_prop, target_prop):
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from .utils import add_direct_driver, get_bioxels_obj
|
|
||||||
# bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
|
||||||
# if bioxels_obj:
|
|
||||||
# container_obj = bioxels_obj.parent
|
|
||||||
# add_direct_driver(
|
|
||||||
# target=node,
|
|
||||||
# target_prop='{target_prop}',
|
|
||||||
# source=container_obj,
|
|
||||||
# source_prop='{source_prop}'
|
|
||||||
# )
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any bioxels.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
# def set_prop_to_node_factory(source_prop, target_prop):
|
|
||||||
# callback_str = f"""
|
|
||||||
# import bpy
|
|
||||||
# from .utils import get_bioxels_obj
|
|
||||||
# bioxels_obj = get_bioxels_obj(bpy.context.active_object)
|
|
||||||
# if bioxels_obj:
|
|
||||||
# container_obj = bioxels_obj.parent
|
|
||||||
# node.inputs.get('{target_prop}').default_value = container_obj.get(
|
|
||||||
# '{source_prop}')
|
|
||||||
# else:
|
|
||||||
# print('Cannot find any bioxels.')
|
|
||||||
# """
|
|
||||||
# return callback_str
|
|
||||||
|
|
||||||
|
|
||||||
NODE_FILE = "BioxelNodes_4.1"
|
|
||||||
|
|
||||||
MENU_ITEMS = [
|
|
||||||
{
|
|
||||||
'label': 'Methods',
|
|
||||||
'icon': 'OUTLINER_DATA_VOLUME',
|
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'label': 'Mask by Threshold',
|
|
||||||
'icon': 'EMPTY_SINGLE_ARROW',
|
|
||||||
'node_type': 'BioxelNodes_MaskByThreshold',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Mask by Range',
|
|
||||||
'icon': 'IPO_CONSTANT',
|
|
||||||
'node_type': 'BioxelNodes_MaskByRange',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Mask by Label',
|
|
||||||
'icon': 'MESH_CAPSULE',
|
|
||||||
'node_type': 'BioxelNodes_MaskByLabel',
|
|
||||||
'node_description': ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Shaders',
|
|
||||||
'icon': 'SHADING_RENDERED',
|
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'label': 'Membrane Shader',
|
|
||||||
'icon': 'NODE_MATERIAL',
|
|
||||||
'node_type': 'BioxelNodes_AssignMembraneShader',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Solid Shader',
|
|
||||||
'icon': 'SHADING_SOLID',
|
|
||||||
'node_type': 'BioxelNodes_AssignSolidShader',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Slime Shader',
|
|
||||||
'icon': 'OUTLINER_DATA_META',
|
|
||||||
'node_type': 'BioxelNodes_AssignSlimeShader',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Volume Shader',
|
|
||||||
'icon': 'VOLUME_DATA',
|
|
||||||
'node_type': 'BioxelNodes_AssignVolumeShader',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Universal Shader',
|
|
||||||
'icon': 'MATSHADERBALL',
|
|
||||||
'node_type': 'BioxelNodes_AssignUniversalShader',
|
|
||||||
'node_description': ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Colors',
|
|
||||||
'icon': 'COLOR',
|
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'label': 'Color Presets',
|
|
||||||
'icon': 'COLOR',
|
|
||||||
'node_type': 'BioxelNodes_ColorPresets',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Color Presets MRI',
|
|
||||||
'icon': 'COLOR',
|
|
||||||
'node_type': 'BioxelNodes_ColorPresets_MRI',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
"separator",
|
|
||||||
{
|
|
||||||
'label': 'Color Ramp 2',
|
|
||||||
'icon': 'IPO_QUAD',
|
|
||||||
'node_type': 'BioxelNodes_SetColorRamp2',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Color Ramp 3',
|
|
||||||
'icon': 'IPO_CUBIC',
|
|
||||||
'node_type': 'BioxelNodes_SetColorRamp3',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Color Ramp 4',
|
|
||||||
'icon': 'IPO_QUART',
|
|
||||||
'node_type': 'BioxelNodes_SetColorRamp4',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Color Ramp 5',
|
|
||||||
'icon': 'IPO_QUINT',
|
|
||||||
'node_type': 'BioxelNodes_SetColorRamp5',
|
|
||||||
'node_description': ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Cutters',
|
|
||||||
'icon': 'MOD_BEVEL',
|
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'label': 'Cut',
|
|
||||||
'icon': 'MOD_BEVEL',
|
|
||||||
'node_type': 'BioxelNodes_Cut',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
"separator",
|
|
||||||
{
|
|
||||||
'label': 'Primitive Cutter',
|
|
||||||
'icon': 'MOD_LINEART',
|
|
||||||
'node_type': 'BioxelNodes_PrimitiveCutter',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
"separator",
|
|
||||||
{
|
|
||||||
'label': 'Plane Cutter',
|
|
||||||
'icon': 'MESH_PLANE',
|
|
||||||
'node_type': 'BioxelNodes_PlaneObjectCutter',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Cylinder Cutter',
|
|
||||||
'icon': 'MESH_CYLINDER',
|
|
||||||
'node_type': 'BioxelNodes_CylinderObjectCutter',
|
|
||||||
'node_description': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Cube Cutter',
|
|
||||||
'icon': 'MESH_CUBE',
|
|
||||||
'node_type': 'BioxelNodes_CubeObjectCutter',
|
|
||||||
'node_description': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Sphere Cutter',
|
|
||||||
'icon': 'MESH_UVSPHERE',
|
|
||||||
'node_type': 'BioxelNodes_SphereObjectCutter',
|
|
||||||
'node_description': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Pie Cutter',
|
|
||||||
'icon': 'MESH_CONE',
|
|
||||||
'node_type': 'BioxelNodes_PieObjectCutter',
|
|
||||||
'node_description': '',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Utils',
|
|
||||||
'icon': 'MODIFIER',
|
|
||||||
'items': [
|
|
||||||
{
|
|
||||||
'label': 'Join Component',
|
|
||||||
'icon': 'CONSTRAINT_BONE',
|
|
||||||
'node_type': 'BioxelNodes_JoinComponent',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
"separator",
|
|
||||||
{
|
|
||||||
'label': 'Pick Mesh',
|
|
||||||
'icon': 'OUTLINER_OB_MESH',
|
|
||||||
'node_type': 'BioxelNodes_PickMesh',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Pick Volume',
|
|
||||||
'icon': 'OUTLINER_OB_VOLUME',
|
|
||||||
'node_type': 'BioxelNodes_PickVolume',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Pick Bbox Wire',
|
|
||||||
'icon': 'MESH_CUBE',
|
|
||||||
'node_type': 'BioxelNodes_PickBboxWire',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
"separator",
|
|
||||||
{
|
|
||||||
'label': 'Inflate',
|
|
||||||
'icon': 'OUTLINER_OB_META',
|
|
||||||
'node_type': 'BioxelNodes_M_Inflate',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Smooth',
|
|
||||||
'icon': 'MOD_SMOOTH',
|
|
||||||
'node_type': 'BioxelNodes_M_Smooth',
|
|
||||||
'node_description': ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'label': 'Remove Small Island',
|
|
||||||
'icon': 'FORCE_LENNARDJONES',
|
|
||||||
'node_type': 'BioxelNodes_M_RemoveSmallIsland',
|
|
||||||
'node_description': ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
custom_nodes = CustomNodes(
|
|
||||||
menu_items=MENU_ITEMS,
|
|
||||||
nodes_file=Path(__file__).parent / f"assets/Nodes/{NODE_FILE}.blend",
|
|
||||||
class_prefix="BIOXELNODES_MT_NODES",
|
|
||||||
root_label='Bioxel Nodes',
|
|
||||||
root_icon="FILE_VOLUME",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
custom_nodes.register()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
custom_nodes.unregister()
|
|
||||||
@@ -1,929 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import random
|
|
||||||
import bpy
|
|
||||||
import pyopenvdb as vdb
|
|
||||||
import numpy as np
|
|
||||||
import bmesh
|
|
||||||
from . import skimage as ski
|
|
||||||
from . import scipy
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
from .utils import (get_container, get_container_from_selection, get_container_layers,
|
|
||||||
get_layer, get_nodes_by_type, hide_in_ray, lock_transform, move_node_between_nodes, move_node_to_node, save_vdb, select_object)
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_name(layer):
|
|
||||||
container = layer.parent
|
|
||||||
return layer.name.removeprefix(container.name).replace("_", "")
|
|
||||||
|
|
||||||
|
|
||||||
def get_grids(layer):
|
|
||||||
layer_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
grids, base_metadata = vdb.readAll(layer_dir)
|
|
||||||
return grids
|
|
||||||
|
|
||||||
|
|
||||||
def set_volume(grids, index, volume):
|
|
||||||
grids[index].clear()
|
|
||||||
grids[index].copyFromArray(volume.copy().astype(np.float32))
|
|
||||||
|
|
||||||
|
|
||||||
def get_volume(grids, index, shape):
|
|
||||||
volume = np.ndarray(shape, np.float32)
|
|
||||||
volume.fill(index)
|
|
||||||
grids[0].copyToArray(volume)
|
|
||||||
return volume
|
|
||||||
|
|
||||||
|
|
||||||
def get_shape(layer):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
return [int(a)
|
|
||||||
for a in layer_node.inputs['Shape'].default_value]
|
|
||||||
|
|
||||||
|
|
||||||
def get_layer_meta(layer, key: str):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
return layer_node.inputs[key].default_value
|
|
||||||
|
|
||||||
|
|
||||||
def set_layer_meta(layer, key: str, value):
|
|
||||||
layer_node = layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
layer_node.inputs[key].default_value = value
|
|
||||||
|
|
||||||
|
|
||||||
def add_mask_node(container, layer, node_type: str, node_label: str):
|
|
||||||
modifier = container.modifiers[0]
|
|
||||||
container_node_group = modifier.node_group
|
|
||||||
|
|
||||||
mask_node = custom_nodes.add_node(container_node_group, node_type)
|
|
||||||
mask_node.label = node_label
|
|
||||||
mask_node.inputs[0].default_value = layer
|
|
||||||
|
|
||||||
# Connect to output if no output linked
|
|
||||||
output_node = get_nodes_by_type(container_node_group,
|
|
||||||
'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
if len(output_node.inputs[0].links) == 0:
|
|
||||||
container_node_group.links.new(mask_node.outputs[0],
|
|
||||||
output_node.inputs[0])
|
|
||||||
move_node_to_node(mask_node, output_node, (-300, 0))
|
|
||||||
else:
|
|
||||||
move_node_to_node(mask_node, output_node, (0, -100))
|
|
||||||
|
|
||||||
return mask_node
|
|
||||||
|
|
||||||
|
|
||||||
def deep_copy_layer(vdb_path, base_layer, name):
|
|
||||||
# Read VDB
|
|
||||||
print(f"Loading the cache to Blender scene...")
|
|
||||||
bpy.ops.object.volume_import(
|
|
||||||
filepath=str(vdb_path), align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
|
|
||||||
copyed_layer = bpy.context.active_object
|
|
||||||
|
|
||||||
# Set props to VDB object
|
|
||||||
copyed_layer.name = name
|
|
||||||
copyed_layer.data.name = name
|
|
||||||
|
|
||||||
lock_transform(copyed_layer)
|
|
||||||
hide_in_ray(copyed_layer)
|
|
||||||
copyed_layer.hide_select = True
|
|
||||||
copyed_layer.hide_render = True
|
|
||||||
copyed_layer.hide_viewport = True
|
|
||||||
# copyed_layer.data.display.use_slice = True
|
|
||||||
# copyed_layer.data.display.density = 1e-05
|
|
||||||
|
|
||||||
copyed_layer['bioxel_layer'] = True
|
|
||||||
copyed_layer['bioxel_layer_type'] = base_layer['bioxel_layer_type']
|
|
||||||
copyed_layer.parent = base_layer.parent
|
|
||||||
|
|
||||||
for collection in copyed_layer.users_collection:
|
|
||||||
collection.objects.unlink(copyed_layer)
|
|
||||||
|
|
||||||
for collection in base_layer.users_collection:
|
|
||||||
collection.objects.link(copyed_layer)
|
|
||||||
|
|
||||||
# add convert to layer node
|
|
||||||
base_layer_node = base_layer.modifiers[0].node_group.nodes['BioxelNodes__ConvertToLayer']
|
|
||||||
|
|
||||||
not_transformed = base_layer_node.inputs['Not Transfromed'].default_value
|
|
||||||
dtype_index = base_layer_node.inputs['Data Type'].default_value
|
|
||||||
bioxel_size = base_layer_node.inputs['Bioxel Size'].default_value
|
|
||||||
layer_shape = base_layer_node.inputs['Shape'].default_value
|
|
||||||
layer_origin = base_layer_node.inputs['Origin'].default_value
|
|
||||||
layer_rotation = base_layer_node.inputs['Rotation'].default_value
|
|
||||||
scalar_offset = base_layer_node.inputs['Scalar Offset'].default_value
|
|
||||||
scalar_min = base_layer_node.inputs['Scalar Min'].default_value
|
|
||||||
scalar_max = base_layer_node.inputs['Scalar Max'].default_value
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
node_group = copyed_layer.modifiers[0].node_group
|
|
||||||
|
|
||||||
input_node = get_nodes_by_type(node_group, 'NodeGroupInput')[0]
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
|
|
||||||
copyed_layer_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes__ConvertToLayer")
|
|
||||||
|
|
||||||
node_group.links.new(input_node.outputs[0], copyed_layer_node.inputs[0])
|
|
||||||
node_group.links.new(copyed_layer_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
# for compatibility to old vdb
|
|
||||||
copyed_layer_node.inputs['Not Transfromed'].default_value = not_transformed
|
|
||||||
copyed_layer_node.inputs['Layer ID'].default_value = random.randint(-200000000,
|
|
||||||
200000000)
|
|
||||||
copyed_layer_node.inputs['Data Type'].default_value = dtype_index
|
|
||||||
copyed_layer_node.inputs['Bioxel Size'].default_value = bioxel_size
|
|
||||||
copyed_layer_node.inputs['Shape'].default_value = layer_shape
|
|
||||||
copyed_layer_node.inputs['Origin'].default_value = layer_origin
|
|
||||||
copyed_layer_node.inputs['Rotation'].default_value = layer_rotation
|
|
||||||
copyed_layer_node.inputs['Scalar Offset'].default_value = scalar_offset
|
|
||||||
copyed_layer_node.inputs['Scalar Min'].default_value = scalar_min
|
|
||||||
copyed_layer_node.inputs['Scalar Max'].default_value = scalar_max
|
|
||||||
|
|
||||||
return copyed_layer
|
|
||||||
|
|
||||||
|
|
||||||
def get_scalar_layer_selection(self, context):
|
|
||||||
items = [("None", "None", "")]
|
|
||||||
container = get_container(bpy.context.active_object)
|
|
||||||
for layer in get_container_layers(container):
|
|
||||||
if layer.get("bioxel_layer_type") == "scalar":
|
|
||||||
items.append((
|
|
||||||
layer.name,
|
|
||||||
layer.name,
|
|
||||||
""
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def get_label_layer_selection(self, context):
|
|
||||||
items = [("None", "None", "")]
|
|
||||||
container = get_container(bpy.context.active_object)
|
|
||||||
for layer in get_container_layers(container):
|
|
||||||
if layer.get("bioxel_layer_type") == "label":
|
|
||||||
items.append((
|
|
||||||
layer.name,
|
|
||||||
layer.name,
|
|
||||||
""
|
|
||||||
))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
# class JoinLayers(bpy.types.Operator):
|
|
||||||
# bl_idname = "bioxelnodes.join_layers"
|
|
||||||
# bl_label = "Join Bioxel Layers"
|
|
||||||
# bl_description = "Join Additional Bioxel Layers"
|
|
||||||
# bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
# base_layer: bpy.props.StringProperty(
|
|
||||||
# options={"HIDDEN"}
|
|
||||||
# ) # type: ignore
|
|
||||||
|
|
||||||
# scalar_layer: bpy.props.EnumProperty(
|
|
||||||
# name="Scalar Layer",
|
|
||||||
# items=get_scalar_layer_selection
|
|
||||||
# ) # type: ignore
|
|
||||||
|
|
||||||
# label_layer: bpy.props.EnumProperty(
|
|
||||||
# name="Label Layer",
|
|
||||||
# items=get_label_layer_selection
|
|
||||||
# ) # type: ignore
|
|
||||||
# # color_layer: bpy.props.StringProperty() # type: ignore
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def poll(cls, context):
|
|
||||||
# layer = get_layer(bpy.context.active_object)
|
|
||||||
# return True if layer else False
|
|
||||||
|
|
||||||
# def execute(self, context):
|
|
||||||
# base_layer = bpy.data.objects[self.base_layer]
|
|
||||||
|
|
||||||
# if not base_layer:
|
|
||||||
# self.report({"WARNING"}, "Cannot find any bioxel layer as base.")
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# base_layer_dir = bpy.path.abspath(base_layer.data.filepath)
|
|
||||||
# base_grids, base_metadata = vdb.readAll(base_layer_dir)
|
|
||||||
|
|
||||||
# layers = []
|
|
||||||
# if self.scalar_layer != "None":
|
|
||||||
# scalar_layer = bpy.data.objects[self.scalar_layer]
|
|
||||||
# layers.append(scalar_layer)
|
|
||||||
|
|
||||||
# if self.label_layer != "None":
|
|
||||||
# label_layer = bpy.data.objects[self.label_layer]
|
|
||||||
# layers.append(label_layer)
|
|
||||||
|
|
||||||
# # TODO: add color and vector
|
|
||||||
|
|
||||||
# if len(layers) == 0:
|
|
||||||
# self.report({"WARNING"}, "No additinal layers setted.")
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# for layer in layers:
|
|
||||||
# layer_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
# grids, metadata = vdb.readAll(layer_dir)
|
|
||||||
# base_grids.extend(grids)
|
|
||||||
|
|
||||||
# vdb_path = save_vdb(base_grids, context)
|
|
||||||
|
|
||||||
# joined_layer = deep_copy_layer(vdb_path,
|
|
||||||
# base_layer,
|
|
||||||
# f"{base_layer.name}_Joined")
|
|
||||||
|
|
||||||
# return {'FINISHED'}
|
|
||||||
|
|
||||||
# def invoke(self, context, event):
|
|
||||||
# base_layer = get_layer(bpy.context.active_object)
|
|
||||||
# self.base_layer = base_layer.name
|
|
||||||
# context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
# return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class InvertScalar(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.invert_scalar"
|
|
||||||
bl_label = "Invert Scalar"
|
|
||||||
bl_description = "Invert the scalar value"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
inverted_layer_name = f"{get_layer_name(base_layer)}_Inverted"
|
|
||||||
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
base_volume = -(base_volume - scalar_offset)
|
|
||||||
|
|
||||||
base_min = float(np.min(base_volume))
|
|
||||||
base_max = float(np.max(base_volume))
|
|
||||||
|
|
||||||
scalar_offset = 0
|
|
||||||
if base_min < 0:
|
|
||||||
scalar_offset = -base_min
|
|
||||||
base_volume = base_volume + scalar_offset
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
inverted_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{inverted_layer_name}")
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Offset', scalar_offset)
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Min', base_min)
|
|
||||||
set_layer_meta(inverted_layer, 'Scalar Max', base_max)
|
|
||||||
|
|
||||||
add_mask_node(container,
|
|
||||||
inverted_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
inverted_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByThreshold(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_threshold"
|
|
||||||
bl_label = "Fill Value by Threshold"
|
|
||||||
bl_description = "Fill Value by Threshold"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
threshold: bpy.props.FloatProperty(
|
|
||||||
name="Threshold",
|
|
||||||
soft_min=0, soft_max=1024,
|
|
||||||
default=128,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Area",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{self.threshold}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
mask = base_volume > (self.threshold + scalar_offset)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
mask_node = add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
mask_node.inputs[1].default_value = self.threshold
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
|
||||||
self.fill_value = min(scalar_min,0)
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByRange(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_range"
|
|
||||||
bl_label = "Fill Value by Range"
|
|
||||||
bl_description = "Fill Value by Range"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Area",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{self.from_min}-{self.from_max}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
mask = (base_volume > self.from_min + scalar_offset) \
|
|
||||||
& (base_volume < self.from_max + scalar_offset)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
mask_node = add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
mask_node.inputs[1].default_value = self.from_min
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
|
||||||
self.fill_value = min(scalar_min,0)
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class FillByLabel(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.fill_by_label"
|
|
||||||
bl_label = "Fill Value by Label"
|
|
||||||
bl_description = "Fill Value by Label Area"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
label_layer: bpy.props.EnumProperty(
|
|
||||||
name="Label Layer",
|
|
||||||
items=get_label_layer_selection
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
fill_value: bpy.props.FloatProperty(
|
|
||||||
name="Fill Value",
|
|
||||||
soft_min=0, soft_max=1024.0,
|
|
||||||
default=0,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
invert: bpy.props.BoolProperty(
|
|
||||||
name="Invert Label",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
layer = get_layer(bpy.context.active_object)
|
|
||||||
if layer:
|
|
||||||
return layer.get("bioxel_layer_type") == "scalar"
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
label_layer = bpy.data.objects[self.label_layer]
|
|
||||||
|
|
||||||
if not label_layer:
|
|
||||||
self.report({"WARNING"}, "Cannot find any label layer.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = base_layer.parent
|
|
||||||
filled_layer_name = f"{get_layer_name(base_layer)}_{get_layer_name(label_layer)}-Filled"
|
|
||||||
scalar_offset = get_layer_meta(base_layer, "Scalar Offset")
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
label_shape = get_shape(label_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
|
|
||||||
label_grids = get_grids(label_layer)
|
|
||||||
mask = get_volume(label_grids, 0, label_shape)
|
|
||||||
mask = ski.resize(mask,
|
|
||||||
base_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=True)
|
|
||||||
mask = scipy.median_filter(mask.astype(np.float32), size=2)
|
|
||||||
|
|
||||||
if self.invert:
|
|
||||||
base_volume = mask * base_volume + \
|
|
||||||
(1-mask) * (self.fill_value + scalar_offset)
|
|
||||||
else:
|
|
||||||
base_volume = (1-mask) * base_volume + \
|
|
||||||
mask * (self.fill_value + scalar_offset)
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
filled_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{filled_layer_name}")
|
|
||||||
add_mask_node(container,
|
|
||||||
filled_layer,
|
|
||||||
"BioxelNodes_MaskByThreshold",
|
|
||||||
filled_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
base_layer = get_layer(bpy.context.active_object)
|
|
||||||
scalar_min = get_layer_meta(base_layer, "Scalar Min")
|
|
||||||
self.fill_value = min(scalar_min,0)
|
|
||||||
context.window_manager.invoke_props_dialog(self, width=400)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class CombineLabels(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.combine_labels"
|
|
||||||
bl_label = "Combine Labels"
|
|
||||||
bl_description = "Combine all selected labels"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
labels = [obj for obj in context.selected_ids
|
|
||||||
if obj.get("bioxel_layer_type") == "label"]
|
|
||||||
return True if len(labels) > 1 else False
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
labels = [obj for obj in context.selected_ids
|
|
||||||
if obj.get("bioxel_layer_type") == "label"]
|
|
||||||
base_layer = labels[0]
|
|
||||||
labels = labels[1:]
|
|
||||||
container = base_layer.parent
|
|
||||||
label_names = [get_layer_name(base_layer)]
|
|
||||||
base_shape = get_shape(base_layer)
|
|
||||||
|
|
||||||
base_grids = get_grids(base_layer)
|
|
||||||
base_volume = get_volume(base_grids, 0, base_shape)
|
|
||||||
base_volume = base_volume
|
|
||||||
|
|
||||||
for label in labels:
|
|
||||||
label_shape = get_shape(label)
|
|
||||||
label_grids = get_grids(label)
|
|
||||||
label_volume = get_volume(label_grids, 0, label_shape)
|
|
||||||
label_volume = ski.resize(label_volume,
|
|
||||||
base_shape,
|
|
||||||
preserve_range=True,
|
|
||||||
anti_aliasing=True)
|
|
||||||
base_volume = np.maximum(base_volume, label_volume)
|
|
||||||
label_names.append(get_layer_name(label))
|
|
||||||
|
|
||||||
set_volume(base_grids, 0, base_volume)
|
|
||||||
|
|
||||||
combined_layer_name = f"{'-'.join(label_names)}-Combined"
|
|
||||||
vdb_path = save_vdb(base_grids, context)
|
|
||||||
combined_layer = deep_copy_layer(vdb_path,
|
|
||||||
base_layer,
|
|
||||||
f"{container.name}_{combined_layer_name}")
|
|
||||||
add_mask_node(container,
|
|
||||||
combined_layer,
|
|
||||||
"BioxelNodes_MaskByLabel",
|
|
||||||
combined_layer_name)
|
|
||||||
|
|
||||||
select_object(container)
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ConvertToMesh(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.convert_to_mesh"
|
|
||||||
bl_label = "Convert To Mesh"
|
|
||||||
bl_description = "Convert Container To Mesh"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
mesh = bpy.context.active_object
|
|
||||||
|
|
||||||
mesh.name = f"Mesh_{container.name}"
|
|
||||||
|
|
||||||
# bpy.ops.object.constraint_add(type='COPY_TRANSFORMS')
|
|
||||||
# mesh.constraints[0].target = container
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = mesh.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
to_mesh_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickMesh")
|
|
||||||
|
|
||||||
to_mesh_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(to_mesh_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
# bpy.ops.constraint.apply(
|
|
||||||
# constraint=mesh.constraints[0].name, owner='OBJECT')
|
|
||||||
bpy.ops.object.modifier_apply(modifier=mesh.modifiers[0].name)
|
|
||||||
|
|
||||||
select_object(mesh)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully convert to mesh")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickMesh(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_mesh"
|
|
||||||
bl_label = "Pick Mesh"
|
|
||||||
bl_description = "Pick Container Mesh"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
mesh = bpy.context.active_object
|
|
||||||
|
|
||||||
mesh.name = f"Mesh_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = mesh.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_mesh_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickMesh")
|
|
||||||
|
|
||||||
pick_mesh_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(pick_mesh_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(mesh)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked mesh")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickVolume(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_volume"
|
|
||||||
bl_label = "Pick Volume"
|
|
||||||
bl_description = "Pick Container Volume"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
volume = bpy.context.active_object
|
|
||||||
|
|
||||||
volume.name = f"Volume_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = volume.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_volume_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickVolume")
|
|
||||||
|
|
||||||
pick_volume_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(
|
|
||||||
pick_volume_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(volume)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked volume")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class PickBboxWire(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.pick_bbox_wire"
|
|
||||||
bl_label = "Pick Bbox Wire"
|
|
||||||
bl_description = "Pick Container Bbox Wire"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
bbox_wire = bpy.context.active_object
|
|
||||||
|
|
||||||
bbox_wire.name = f"Wire_{container.name}"
|
|
||||||
|
|
||||||
bpy.ops.node.new_geometry_nodes_modifier()
|
|
||||||
modifier = bbox_wire.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
pick_bbox_wire_node = custom_nodes.add_node(node_group,
|
|
||||||
"BioxelNodes_PickBboxWire")
|
|
||||||
|
|
||||||
pick_bbox_wire_node.inputs[0].default_value = container
|
|
||||||
node_group.links.new(
|
|
||||||
pick_bbox_wire_node.outputs[0], output_node.inputs[0])
|
|
||||||
|
|
||||||
select_object(bbox_wire)
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully picked bbox wire")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class AddCutter():
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
container = containers[0]
|
|
||||||
|
|
||||||
if self.object_type == "plane":
|
|
||||||
node_type = "BioxelNodes_PlaneObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_plane_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "cylinder":
|
|
||||||
node_type = "BioxelNodes_CylinderObjectCutter"
|
|
||||||
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.rotation_euler[0]
|
|
||||||
elif self.object_type == "cube":
|
|
||||||
node_type = "BioxelNodes_CubeObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_cube_add(
|
|
||||||
size=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "sphere":
|
|
||||||
node_type = "BioxelNodes_SphereObjectCutter"
|
|
||||||
bpy.ops.mesh.primitive_ico_sphere_add(
|
|
||||||
radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1))
|
|
||||||
elif self.object_type == "pie":
|
|
||||||
node_type = "BioxelNodes_PieObjectCutter"
|
|
||||||
# Create mesh
|
|
||||||
pie_mesh = bpy.data.meshes.new('Pie')
|
|
||||||
|
|
||||||
# Create object
|
|
||||||
pie = bpy.data.objects.new('Pie', pie_mesh)
|
|
||||||
|
|
||||||
# Link object to scene
|
|
||||||
bpy.context.scene.collection.objects.link(pie)
|
|
||||||
# Get a BMesh representation
|
|
||||||
bm = bmesh.new() # create an empty BMesh
|
|
||||||
bm.from_mesh(pie_mesh) # fill it in from a Mesh
|
|
||||||
|
|
||||||
# Hot to create vertices
|
|
||||||
v_0 = bm.verts.new((0.0, -1.0, 0.0))
|
|
||||||
v_1 = bm.verts.new((-1.0, -1.0, 1.0))
|
|
||||||
v_2 = bm.verts.new((0.0, 1.0, 0.0))
|
|
||||||
v_3 = bm.verts.new((-1.0, 1.0, 1.0))
|
|
||||||
v_4 = bm.verts.new((1.0, -1.0, 1.0))
|
|
||||||
v_5 = bm.verts.new((1.0, 1.0, 1.0))
|
|
||||||
|
|
||||||
# Initialize the index values of this sequence.
|
|
||||||
bm.verts.index_update()
|
|
||||||
|
|
||||||
# How to create a face
|
|
||||||
# it's not necessary to create the edges before, I made it only to show how create
|
|
||||||
# edges too
|
|
||||||
bm.faces.new((v_0, v_1, v_3, v_2))
|
|
||||||
bm.faces.new((v_0, v_2, v_5, v_4))
|
|
||||||
|
|
||||||
# Finish up, write the bmesh back to the mesh
|
|
||||||
bm.to_mesh(pie_mesh)
|
|
||||||
bpy.context.view_layer.objects.active = pie
|
|
||||||
|
|
||||||
cutter = bpy.context.active_object
|
|
||||||
cutter.visible_camera = False
|
|
||||||
cutter.visible_diffuse = False
|
|
||||||
cutter.visible_glossy = False
|
|
||||||
cutter.visible_transmission = False
|
|
||||||
cutter.visible_volume_scatter = False
|
|
||||||
cutter.visible_shadow = False
|
|
||||||
cutter.hide_render = True
|
|
||||||
cutter.display_type = 'WIRE'
|
|
||||||
|
|
||||||
modifier = container.modifiers[0]
|
|
||||||
node_group = modifier.node_group
|
|
||||||
cutter_node = custom_nodes.add_node(node_group, node_type)
|
|
||||||
cutter_node.inputs[0].default_value = cutter
|
|
||||||
|
|
||||||
cut_nodes = get_nodes_by_type(node_group,
|
|
||||||
'BioxelNodes_Cut')
|
|
||||||
output_node = get_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
|
||||||
if len(cut_nodes) == 0:
|
|
||||||
cut_node = custom_nodes.add_node(node_group, 'BioxelNodes_Cut')
|
|
||||||
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))
|
|
||||||
select_object(cutter)
|
|
||||||
else:
|
|
||||||
move_node_to_node(cutter_node, output_node, (0, -100))
|
|
||||||
select_object(container)
|
|
||||||
|
|
||||||
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 Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "plane"
|
|
||||||
|
|
||||||
|
|
||||||
class AddCylinderCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_cylinder_cutter"
|
|
||||||
bl_label = "Add a Cylinder Cutter"
|
|
||||||
bl_description = "Add a Cylinder Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "cylinder"
|
|
||||||
|
|
||||||
|
|
||||||
class AddCubeCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_cube_cutter"
|
|
||||||
bl_label = "Add a Cube Cutter"
|
|
||||||
bl_description = "Add a Cube Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "cube"
|
|
||||||
|
|
||||||
|
|
||||||
class AddSphereCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_sphere_cutter"
|
|
||||||
bl_label = "Add a Sphere Cutter"
|
|
||||||
bl_description = "Add a Sphere Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "sphere"
|
|
||||||
|
|
||||||
|
|
||||||
class AddPieCutter(bpy.types.Operator, AddCutter):
|
|
||||||
bl_idname = "bioxelnodes.add_pie_cutter"
|
|
||||||
bl_label = "Add a Pie Cutter"
|
|
||||||
bl_description = "Add a Pie Cutter to Container"
|
|
||||||
bl_options = {'UNDO'}
|
|
||||||
object_type = "pie"
|
|
||||||
0
bioxelnodes/operators/__init__.py
Normal file
0
bioxelnodes/operators/__init__.py
Normal file
525
bioxelnodes/operators/container.py
Normal file
525
bioxelnodes/operators/container.py
Normal file
@@ -0,0 +1,525 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
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_nodes_by_type, 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 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_nodes_by_type(node_group, 'NodeGroupOutput')[0]
|
||||||
|
fetch_mesh_node = add_node_to_graph(f"Fetch{self.object_type}",
|
||||||
|
node_group,
|
||||||
|
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])
|
||||||
|
|
||||||
|
select_object(obj)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully picked")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractSurface(bpy.types.Operator, ExtractObject):
|
||||||
|
bl_idname = "bioxelnodes.fetch_mesh"
|
||||||
|
bl_label = "Extract Surface"
|
||||||
|
bl_description = "Extract Surface"
|
||||||
|
bl_icon = "OUTLINER_OB_MESH"
|
||||||
|
object_type = "Surface"
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractVolume(bpy.types.Operator, ExtractObject):
|
||||||
|
bl_idname = "bioxelnodes.pick_volume"
|
||||||
|
bl_label = "Extract Volume"
|
||||||
|
bl_description = "Extract Volume"
|
||||||
|
bl_icon = "OUTLINER_OB_VOLUME"
|
||||||
|
object_type = "Volume"
|
||||||
|
|
||||||
|
|
||||||
|
class ExtractShapeWire(bpy.types.Operator, ExtractObject):
|
||||||
|
bl_idname = "bioxelnodes.pick_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.pick_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 = self.cutter_type.capitalize()
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
get_use_link())
|
||||||
|
|
||||||
|
output_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
if len(output_node.inputs[0].links) == 0:
|
||||||
|
node_group.links.new(cut_node.outputs[0],
|
||||||
|
output_node.inputs[0])
|
||||||
|
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 = name
|
||||||
|
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,
|
||||||
|
get_use_link())
|
||||||
|
|
||||||
|
slice_node.inputs[1].default_value = slicer_obj
|
||||||
|
|
||||||
|
output_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
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
|
||||||
|
|
||||||
|
parent_node = add_node_to_graph("TransformParent",
|
||||||
|
node_group,
|
||||||
|
get_use_link())
|
||||||
|
|
||||||
|
parent_node.inputs[1].default_value = locator_obj
|
||||||
|
|
||||||
|
output_node = get_nodes_by_type(node_group,
|
||||||
|
'NodeGroupOutput')[0]
|
||||||
|
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])
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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'}
|
||||||
967
bioxelnodes/operators/io.py
Normal file
967
bioxelnodes/operators/io.py
Normal file
@@ -0,0 +1,967 @@
|
|||||||
|
import math
|
||||||
|
import bpy
|
||||||
|
import shutil
|
||||||
|
import threading
|
||||||
|
import numpy as np
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..exceptions import CancelledByUser
|
||||||
|
from ..props import BIOXELNODES_Series
|
||||||
|
from ..bioxelutils.common import (get_all_layer_objs, get_container_obj,
|
||||||
|
get_layer_obj, is_incompatible)
|
||||||
|
from ..bioxelutils.container import (Container,
|
||||||
|
add_layers,
|
||||||
|
container_to_obj)
|
||||||
|
from ..bioxel.layer import Layer
|
||||||
|
from ..bioxel.parse import (DICOM_EXTS, SUPPORT_EXTS,
|
||||||
|
get_ext, parse_volumetric_data)
|
||||||
|
|
||||||
|
from ..utils import (get_cache_dir,
|
||||||
|
progress_update, progress_bar,
|
||||||
|
select_object)
|
||||||
|
|
||||||
|
# 3rd-party
|
||||||
|
import SimpleITK as sitk
|
||||||
|
import transforms3d
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_shape(bioxel_size: float, orig_shape: tuple, orig_spacing: tuple):
|
||||||
|
shape = (int(orig_shape[0] / bioxel_size * orig_spacing[0]),
|
||||||
|
int(orig_shape[1] / bioxel_size * orig_spacing[1]),
|
||||||
|
int(orig_shape[2] / bioxel_size * orig_spacing[2]))
|
||||||
|
|
||||||
|
return (shape[0] if shape[0] > 0 else 1,
|
||||||
|
shape[1] if shape[1] > 0 else 1,
|
||||||
|
shape[2] if shape[2] > 0 else 1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_layer_size(shape: tuple, bioxel_size: float, scale: float = 1.0):
|
||||||
|
size = (float(shape[0] * bioxel_size * scale),
|
||||||
|
float(shape[1] * bioxel_size * scale),
|
||||||
|
float(shape[2] * bioxel_size * scale))
|
||||||
|
|
||||||
|
return size
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
ImportVolumetricData
|
||||||
|
-> ParseVolumetricData -> ImportVolumetricDataDialog
|
||||||
|
FH_ImportVolumetricData
|
||||||
|
|
||||||
|
start import parse data execute import
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class ImportVolumetricData():
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
read_as = "SCALAR"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
data_path = Path(self.filepath).resolve()
|
||||||
|
ext = get_ext(data_path)
|
||||||
|
if ext not in SUPPORT_EXTS:
|
||||||
|
self.report({"WARNING"}, "Not supported format.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
bpy.ops.bioxelnodes.parse_volumetric_data('INVOKE_DEFAULT',
|
||||||
|
filepath=self.filepath,
|
||||||
|
skip_read_as=True,
|
||||||
|
read_as=self.read_as)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
class ImportAsScalar(bpy.types.Operator, ImportVolumetricData):
|
||||||
|
bl_idname = "bioxelnodes.import_as_scalar"
|
||||||
|
bl_label = "Import as Scalar"
|
||||||
|
bl_description = "Import Volumetric Data to Container as Scalar"
|
||||||
|
bl_icon = "EVENT_S"
|
||||||
|
read_as = "SCALAR"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportAsLabel(bpy.types.Operator, ImportVolumetricData):
|
||||||
|
bl_idname = "bioxelnodes.import_as_label"
|
||||||
|
bl_label = "Import as Label"
|
||||||
|
bl_description = "Import Volumetric Data to Container as Label"
|
||||||
|
bl_icon = "EVENT_L"
|
||||||
|
read_as = "LABEL"
|
||||||
|
|
||||||
|
|
||||||
|
class ImportAsColor(bpy.types.Operator, ImportVolumetricData):
|
||||||
|
bl_idname = "bioxelnodes.import_as_color"
|
||||||
|
bl_label = "Import as Label"
|
||||||
|
bl_description = "Import Volumetric Data to Container as Label"
|
||||||
|
bl_icon = "EVENT_C"
|
||||||
|
read_as = "COLOR"
|
||||||
|
|
||||||
|
|
||||||
|
class BIOXELNODES_FH_ImportVolumetricData(bpy.types.FileHandler):
|
||||||
|
bl_idname = "BIOXELNODES_FH_ImportVolumetricData"
|
||||||
|
bl_label = "File handler for dicom import"
|
||||||
|
bl_import_operator = "bioxelnodes.parse_volumetric_data"
|
||||||
|
bl_file_extensions = ";".join(SUPPORT_EXTS)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll_drop(cls, context):
|
||||||
|
if not context.area:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if context.area.type == 'VIEW_3D':
|
||||||
|
return True
|
||||||
|
elif context.area.type == 'NODE_EDITOR':
|
||||||
|
container_obj = get_container_obj(context.object)
|
||||||
|
return container_obj is not None
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_series_ids(self, context):
|
||||||
|
items = []
|
||||||
|
for index, series_id in enumerate(self.series_ids):
|
||||||
|
items.append((
|
||||||
|
series_id.id,
|
||||||
|
series_id.label,
|
||||||
|
"",
|
||||||
|
index
|
||||||
|
))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
class ParseVolumetricData(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.parse_volumetric_data"
|
||||||
|
bl_label = "Import Volumetric Data (BioxelNodes)"
|
||||||
|
bl_description = "Import Volumetric Data as Layer"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
meta = None
|
||||||
|
label_count = 0
|
||||||
|
dtype = None
|
||||||
|
thread = None
|
||||||
|
_timer = None
|
||||||
|
|
||||||
|
progress: bpy.props.FloatProperty(name="Progress",
|
||||||
|
options={"SKIP_SAVE"},
|
||||||
|
default=1) # type: ignore
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
skip_read_as: bpy.props.BoolProperty(name="Skip Read As",
|
||||||
|
default=False,
|
||||||
|
options={"SKIP_SAVE"}) # type: ignore
|
||||||
|
|
||||||
|
skip_series_select: bpy.props.BoolProperty(name="Skip Sries Select",
|
||||||
|
default=True,
|
||||||
|
options={"SKIP_SAVE"}) # type: ignore
|
||||||
|
|
||||||
|
read_as: bpy.props.EnumProperty(name="Read as",
|
||||||
|
default="SCALAR",
|
||||||
|
items=[("SCALAR", "Scalar", ""),
|
||||||
|
("LABEL", "Labels", ""),
|
||||||
|
("COLOR", "Color", "")]) # type: ignore
|
||||||
|
|
||||||
|
series_id: bpy.props.EnumProperty(name="Select Series",
|
||||||
|
items=get_series_ids) # type: ignore
|
||||||
|
|
||||||
|
series_ids: bpy.props.CollectionProperty(
|
||||||
|
type=BIOXELNODES_Series) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if is_incompatible():
|
||||||
|
self.report({"ERROR"},
|
||||||
|
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if not self.filepath:
|
||||||
|
self.report({"WARNING"}, "No file selected.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
data_path = Path(self.filepath).resolve()
|
||||||
|
ext = get_ext(data_path)
|
||||||
|
if ext not in SUPPORT_EXTS:
|
||||||
|
self.report({"WARNING"}, "Not supported format.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
print("Collecting Meta Data...")
|
||||||
|
|
||||||
|
def parse_volumetric_data_func(self, context, cancel):
|
||||||
|
def progress_callback(factor, text):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
progress_update(context, factor, text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
series_id = self.series_id if self.series_id != "empty" else ""
|
||||||
|
data, meta = parse_volumetric_data(data_file=self.filepath,
|
||||||
|
series_id=series_id,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.meta = meta
|
||||||
|
self.label_count = int(np.max(data))
|
||||||
|
self.dtype = data.dtype
|
||||||
|
|
||||||
|
# Init cancel flag
|
||||||
|
self.is_cancelled = False
|
||||||
|
self.has_error = None
|
||||||
|
|
||||||
|
# Create the thread
|
||||||
|
self.thread = threading.Thread(target=parse_volumetric_data_func,
|
||||||
|
args=(self, context, lambda: self.is_cancelled))
|
||||||
|
|
||||||
|
# Start the thread
|
||||||
|
self.thread.start()
|
||||||
|
# Add a timmer for modal
|
||||||
|
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
||||||
|
window=context.window)
|
||||||
|
# Append progress bar to status bar
|
||||||
|
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
||||||
|
|
||||||
|
# Start modal handler
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
# Check if user press 'ESC'
|
||||||
|
if event.type == 'ESC':
|
||||||
|
self.is_cancelled = True
|
||||||
|
progress_update(context, 0.0, "Canceling...")
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Check if is the timer time
|
||||||
|
if event.type != 'TIMER':
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Force update status bar
|
||||||
|
bpy.context.workspace.status_text_set_internal(None)
|
||||||
|
|
||||||
|
# Check if thread is still running
|
||||||
|
if self.thread.is_alive():
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
# Release the thread
|
||||||
|
self.thread.join()
|
||||||
|
# Remove the timer
|
||||||
|
context.window_manager.event_timer_remove(self._timer)
|
||||||
|
# Remove the progress bar from status bar
|
||||||
|
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||||
|
progress_update(context, 1.0)
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.is_cancelled:
|
||||||
|
self.report({"WARNING"}, "Canncelled by user.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.has_error:
|
||||||
|
raise self.has_error
|
||||||
|
|
||||||
|
# Check if has return
|
||||||
|
if self.meta is None:
|
||||||
|
self.report({"ERROR"}, "Some thing went wrong.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# If not canncelled...
|
||||||
|
for key, value in self.meta.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
|
||||||
|
if self.read_as == "LABEL":
|
||||||
|
if self.label_count > 100 or self.dtype.kind not in ["i", "u"]:
|
||||||
|
self.report({"ERROR"}, "Invaild label data.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
orig_shape = self.meta['xyz_shape']
|
||||||
|
orig_spacing = self.meta['spacing']
|
||||||
|
|
||||||
|
min_log10 = math.floor(math.log10(min(*orig_spacing)))
|
||||||
|
max_log10 = math.floor(math.log10(max(*orig_spacing)))
|
||||||
|
# min_space = min(*orig_spacing)
|
||||||
|
# max_space = max(*orig_spacing)
|
||||||
|
|
||||||
|
if orig_spacing[2] == 1 and min_log10 < -1:
|
||||||
|
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-2),
|
||||||
|
orig_spacing[1] * math.pow(10, -min_log10-2),
|
||||||
|
1)
|
||||||
|
elif min_log10 > 0:
|
||||||
|
orig_spacing = (orig_spacing[0] * math.pow(10, -min_log10-1),
|
||||||
|
orig_spacing[1] * math.pow(10, -min_log10-1),
|
||||||
|
orig_spacing[2] * math.pow(10, -min_log10-1))
|
||||||
|
elif max_log10 < 0:
|
||||||
|
orig_spacing = (orig_spacing[0] * math.pow(10, -max_log10-1),
|
||||||
|
orig_spacing[1] * math.pow(10, -max_log10-1),
|
||||||
|
orig_spacing[2] * math.pow(10, -max_log10-1))
|
||||||
|
|
||||||
|
bioxel_size = max(min(*orig_spacing), 1.0)
|
||||||
|
|
||||||
|
layer_shape = get_layer_shape(bioxel_size,
|
||||||
|
orig_shape,
|
||||||
|
orig_spacing)
|
||||||
|
layer_size = get_layer_size(layer_shape,
|
||||||
|
bioxel_size,
|
||||||
|
0.01)
|
||||||
|
min_log10 = math.floor(math.log10(min(*layer_size)))
|
||||||
|
max_log10 = math.floor(math.log10(max(*layer_size)))
|
||||||
|
|
||||||
|
if min_log10 > 0:
|
||||||
|
scene_scale = math.pow(10, -min_log10-2)
|
||||||
|
elif max_log10 < 0:
|
||||||
|
scene_scale = math.pow(10, -max_log10-2)
|
||||||
|
else:
|
||||||
|
scene_scale = 0.01
|
||||||
|
|
||||||
|
if context.area.type == "NODE_EDITOR":
|
||||||
|
container_obj = context.object
|
||||||
|
container_name = container_obj.name
|
||||||
|
container_obj_name = container_name
|
||||||
|
else:
|
||||||
|
container_name = self.meta['name']
|
||||||
|
container_obj_name = ""
|
||||||
|
|
||||||
|
series_id = self.series_id if self.series_id != "empty" else ""
|
||||||
|
bpy.ops.bioxelnodes.import_volumetric_data_dialog(
|
||||||
|
'INVOKE_DEFAULT',
|
||||||
|
filepath=self.filepath,
|
||||||
|
container_name=container_name,
|
||||||
|
layer_name=self.meta['description'],
|
||||||
|
orig_shape=orig_shape,
|
||||||
|
orig_spacing=orig_spacing,
|
||||||
|
bioxel_size=bioxel_size,
|
||||||
|
series_id=series_id,
|
||||||
|
frame_count=self.meta['frame_count'],
|
||||||
|
channel_count=self.meta['channel_count'],
|
||||||
|
container_obj_name=container_obj_name,
|
||||||
|
read_as=self.read_as,
|
||||||
|
label_count=self.label_count,
|
||||||
|
scene_scale=scene_scale
|
||||||
|
)
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Successfully Readed.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# why not report in execute?
|
||||||
|
# If this operator is executing, a new execute will when pre-one done.
|
||||||
|
if context.window_manager.bioxelnodes_progress_factor < 1:
|
||||||
|
print("A process is executing, please wait for it to finish.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if not self.filepath:
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
data_path = Path(self.filepath).resolve()
|
||||||
|
ext = get_ext(data_path)
|
||||||
|
|
||||||
|
if context.area.type == "NODE_EDITOR":
|
||||||
|
title = f"Add to **{context.object.name}**"
|
||||||
|
else:
|
||||||
|
title = "Init a Container"
|
||||||
|
|
||||||
|
# Series Selection
|
||||||
|
if ext in DICOM_EXTS:
|
||||||
|
data_dirpath = data_path.parent
|
||||||
|
reader = sitk.ImageSeriesReader()
|
||||||
|
reader.MetaDataDictionaryArrayUpdateOn()
|
||||||
|
reader.LoadPrivateTagsOn()
|
||||||
|
|
||||||
|
series_ids = reader.GetGDCMSeriesIDs(str(data_dirpath))
|
||||||
|
series_items = {}
|
||||||
|
|
||||||
|
for series_id in series_ids:
|
||||||
|
series_files = reader.GetGDCMSeriesFileNames(
|
||||||
|
str(data_dirpath), series_id)
|
||||||
|
single = sitk.ImageFileReader()
|
||||||
|
single.SetFileName(series_files[0])
|
||||||
|
single.LoadPrivateTagsOn()
|
||||||
|
single.ReadImageInformation()
|
||||||
|
|
||||||
|
def get_meta(key):
|
||||||
|
try:
|
||||||
|
stirng = single.GetMetaData(key).removesuffix(" ")
|
||||||
|
if stirng in ["No study description",
|
||||||
|
"No series description",
|
||||||
|
""]:
|
||||||
|
return "Unknown"
|
||||||
|
else:
|
||||||
|
return stirng
|
||||||
|
except:
|
||||||
|
return "Unknown"
|
||||||
|
|
||||||
|
study_description = get_meta("0008|1030")
|
||||||
|
series_description = get_meta("0008|103e")
|
||||||
|
series_modality = get_meta("0008|0060")
|
||||||
|
size_x = get_meta("0028|0011")
|
||||||
|
size_y = get_meta("0028|0010")
|
||||||
|
count = get_meta("0020|0013")
|
||||||
|
count = count if count != "Unknown" else 1
|
||||||
|
|
||||||
|
# some series image count = 0 ????
|
||||||
|
if int(count) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# series_id cannot be "" in blender selection
|
||||||
|
if series_id == "":
|
||||||
|
series_id = "empty"
|
||||||
|
|
||||||
|
series_items[series_id] = "{:<20} {:>1}".format(f"{study_description}>{series_description}({series_modality})",
|
||||||
|
f"({size_x}x{size_y})x{count}")
|
||||||
|
|
||||||
|
for key, value in series_items.items():
|
||||||
|
series_item = self.series_ids.add()
|
||||||
|
series_item.id = key
|
||||||
|
series_item.label = value
|
||||||
|
|
||||||
|
if len(series_items.keys()) > 1:
|
||||||
|
self.skip_series_select = False
|
||||||
|
context.window_manager.invoke_props_dialog(self,
|
||||||
|
width=400,
|
||||||
|
title=title)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
elif len(series_items.keys()) == 1:
|
||||||
|
self.series_id = list(series_items.keys())[0]
|
||||||
|
else:
|
||||||
|
self.report({"ERROR"}, "Get no vaild series.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if self.skip_read_as:
|
||||||
|
return self.execute(context)
|
||||||
|
else:
|
||||||
|
context.window_manager.invoke_props_dialog(self,
|
||||||
|
width=400,
|
||||||
|
title=title)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
if not self.skip_read_as:
|
||||||
|
layout.label(
|
||||||
|
text='What kind is the data read as?')
|
||||||
|
layout.prop(self, "read_as")
|
||||||
|
if not self.skip_series_select:
|
||||||
|
layout.label(text='Which series to import?')
|
||||||
|
layout.prop(self, "series_id")
|
||||||
|
|
||||||
|
|
||||||
|
def get_frame_sources(self, context):
|
||||||
|
items = [("-1", "First Frame (1 frame)", "")]
|
||||||
|
orig_shape = tuple(self.orig_shape)
|
||||||
|
if self.frame_count > 1:
|
||||||
|
items.append(("0", f"Frames ({self.frame_count} frames)", ""))
|
||||||
|
elif self.frame_count == 1 and self.channel_count > 1:
|
||||||
|
items.append(("4", f"Channels ({self.channel_count} frames)", ""))
|
||||||
|
elif self.frame_count == 1 and self.channel_count == 1:
|
||||||
|
items.append(("1", f"X-axis ({orig_shape[0]} frames)", ""))
|
||||||
|
items.append(("2", f"Y-axis ({orig_shape[1]} frames)", ""))
|
||||||
|
items.append(("3", f"Z-axis ({orig_shape[2]} frames)", ""))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
class ImportVolumetricDataDialog(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.import_volumetric_data_dialog"
|
||||||
|
bl_label = "Import Volumetric Data"
|
||||||
|
bl_description = "Import Volumetric Data as Layer"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
layers = None
|
||||||
|
thread = None
|
||||||
|
_timer = None
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
container_name: bpy.props.StringProperty(
|
||||||
|
name="Container Name") # type: ignore
|
||||||
|
|
||||||
|
layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore
|
||||||
|
|
||||||
|
series_id: bpy.props.StringProperty() # type: ignore
|
||||||
|
|
||||||
|
container_obj_name: bpy.props.StringProperty() # type: ignore
|
||||||
|
|
||||||
|
frame_count: bpy.props.IntProperty() # type: ignore
|
||||||
|
|
||||||
|
channel_count: bpy.props.IntProperty() # type: ignore
|
||||||
|
|
||||||
|
label_count: bpy.props.IntProperty() # type: ignore
|
||||||
|
|
||||||
|
read_as: bpy.props.EnumProperty(name="Read as",
|
||||||
|
default="SCALAR",
|
||||||
|
items=[("SCALAR", "Scalar", ""),
|
||||||
|
("LABEL", "Labels", ""),
|
||||||
|
("COLOR", "Color", "")]) # type: ignore
|
||||||
|
|
||||||
|
bioxel_size: bpy.props.FloatProperty(name="Bioxel Size (Larger size means small resolution)",
|
||||||
|
soft_min=0.1, soft_max=10.0,
|
||||||
|
min=0.1, max=1e2,
|
||||||
|
default=1) # type: ignore
|
||||||
|
|
||||||
|
orig_spacing: bpy.props.FloatVectorProperty(name="Original Spacing",
|
||||||
|
default=(1, 1, 1)) # type: ignore
|
||||||
|
|
||||||
|
orig_shape: bpy.props.IntVectorProperty(name="Original Shape",
|
||||||
|
default=(100, 100, 100)) # type: ignore
|
||||||
|
|
||||||
|
scene_scale: bpy.props.FloatProperty(name="Scene Scale (Bioxel Unit pre Blender Unit)",
|
||||||
|
soft_min=0.0001, soft_max=10.0,
|
||||||
|
min=1e-6, max=1e6,
|
||||||
|
default=0.01) # type: ignore
|
||||||
|
|
||||||
|
remap: bpy.props.BoolProperty(name="Remap to 0~1",
|
||||||
|
default=False) # type: ignore
|
||||||
|
|
||||||
|
split_channel: bpy.props.BoolProperty(name="Split Channels",
|
||||||
|
default=False) # type: ignore
|
||||||
|
|
||||||
|
frame_source: bpy.props.EnumProperty(name="Frame From",
|
||||||
|
items=get_frame_sources) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
def import_volumetric_data_func(self, context, cancel):
|
||||||
|
progress_update(context, 0.0, "Parsing Volumetirc Data...")
|
||||||
|
|
||||||
|
def progress_callback(factor, text):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
progress_update(context, factor*0.2, text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data, meta = parse_volumetric_data(data_file=self.filepath,
|
||||||
|
series_id=self.series_id,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
shape = get_layer_shape(self.bioxel_size,
|
||||||
|
self.orig_shape,
|
||||||
|
self.orig_spacing)
|
||||||
|
|
||||||
|
mat_scale = transforms3d.zooms.zfdir2aff(self.bioxel_size)
|
||||||
|
affine = np.dot(meta['affine'], mat_scale)
|
||||||
|
kind = self.read_as.lower()
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
# change shape as sequence or not
|
||||||
|
if self.frame_source == "-1":
|
||||||
|
data = data[0:1, :, :, :, :]
|
||||||
|
elif self.frame_source == "0":
|
||||||
|
# frame as frame
|
||||||
|
pass
|
||||||
|
elif self.frame_source == "1":
|
||||||
|
# X as frame
|
||||||
|
data = data.transpose(1, 0, 2, 3, 4)
|
||||||
|
shape = (1, shape[1], shape[2])
|
||||||
|
elif self.frame_source == "2":
|
||||||
|
# Y as frame
|
||||||
|
data = data.transpose(2, 1, 0, 3, 4)
|
||||||
|
shape = (shape[0], 1, shape[2])
|
||||||
|
elif self.frame_source == "3":
|
||||||
|
# Z as frame
|
||||||
|
data = data.transpose(3, 1, 2, 0, 4)
|
||||||
|
shape = (shape[0], shape[1], 1)
|
||||||
|
else:
|
||||||
|
# channel as frame
|
||||||
|
data = data.transpose(4, 1, 2, 3, 0)
|
||||||
|
|
||||||
|
def progress_callback_factory(layer_name, progress, progress_step):
|
||||||
|
def progress_callback(frame, total):
|
||||||
|
if cancel():
|
||||||
|
raise CancelledByUser
|
||||||
|
sub_progress_step = progress_step/total
|
||||||
|
sub_progress = progress + frame * sub_progress_step
|
||||||
|
progress_update(context, sub_progress,
|
||||||
|
f"Processing {layer_name} Frame {frame+1}...")
|
||||||
|
print(f"Processing {layer_name} Frame {frame+1}...")
|
||||||
|
return progress_callback
|
||||||
|
|
||||||
|
layers = []
|
||||||
|
if kind == "label":
|
||||||
|
name = self.layer_name or "Label"
|
||||||
|
data = data.astype(int)
|
||||||
|
label_count = int(np.max(data))
|
||||||
|
progress_step = 0.7/label_count
|
||||||
|
|
||||||
|
for i in range(label_count):
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
name_i = f"{name}_{i+1}"
|
||||||
|
progress = 0.2+i*progress_step
|
||||||
|
progress_update(context, progress,
|
||||||
|
f"Processing {name_i}...")
|
||||||
|
|
||||||
|
progress_callback = progress_callback_factory(name_i,
|
||||||
|
progress,
|
||||||
|
progress_step)
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data == np.full_like(data, i+1),
|
||||||
|
name=name_i,
|
||||||
|
kind=kind)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layer.affine = affine
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if kind == "color":
|
||||||
|
if np.issubdtype(np.uint8, data.dtype):
|
||||||
|
data = np.multiply(data, 1.0 / 256,
|
||||||
|
dtype=np.float32)
|
||||||
|
elif data.dtype.kind in ['u', 'i']:
|
||||||
|
# Convert the normalized array to float dtype
|
||||||
|
data = data.astype(np.float32)
|
||||||
|
|
||||||
|
min_val = data.min()
|
||||||
|
max_val = data.max()
|
||||||
|
# Avoid division by zero if all values are the same
|
||||||
|
if max_val != min_val:
|
||||||
|
# Normalize the array to the range (0,1)
|
||||||
|
data = (data - min_val) / (max_val - min_val)
|
||||||
|
else:
|
||||||
|
# If all values are the same, the normalized array will be all zeros
|
||||||
|
data = np.zeros_like(data, dtype=np.float32)
|
||||||
|
|
||||||
|
else:
|
||||||
|
data = data.astype(np.float32)
|
||||||
|
|
||||||
|
name = self.layer_name or "Color"
|
||||||
|
if data.shape[4] == 1:
|
||||||
|
data = np.repeat(data, repeats=3, axis=4)
|
||||||
|
elif data.shape[4] == 2:
|
||||||
|
d_shape = list(data.shape)
|
||||||
|
d_shape = d_shape[:4] + [1]
|
||||||
|
zore = np.zeros(tuple(d_shape), dtype=np.float32)
|
||||||
|
data = np.concatenate((data, zore), axis=-1)
|
||||||
|
elif data.shape[4] > 3:
|
||||||
|
data = data[:, :, :, :, :3]
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
progress_update(context, 0.2,
|
||||||
|
f"Processing {name}...")
|
||||||
|
progress_callback = progress_callback_factory(name,
|
||||||
|
0.2,
|
||||||
|
0.7)
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data,
|
||||||
|
name=name,
|
||||||
|
kind=kind)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layer.affine = affine
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
elif kind == "scalar":
|
||||||
|
name = self.layer_name or "Scalar"
|
||||||
|
|
||||||
|
if self.remap:
|
||||||
|
# Convert the normalized array to float dtype
|
||||||
|
data = data.astype(np.float32)
|
||||||
|
|
||||||
|
min_val = data.min()
|
||||||
|
max_val = data.max()
|
||||||
|
# Avoid division by zero if all values are the same
|
||||||
|
if max_val != min_val:
|
||||||
|
# Normalize the array to the range (0,1)
|
||||||
|
data = (data - min_val) / (max_val - min_val)
|
||||||
|
else:
|
||||||
|
# If all values are the same, the normalized array will be all zeros
|
||||||
|
data = np.zeros_like(data, dtype=np.float32)
|
||||||
|
|
||||||
|
if self.split_channel:
|
||||||
|
progress_step = 0.7/self.channel_count
|
||||||
|
|
||||||
|
for i in range(self.channel_count):
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
name_i = f"{name}_{i+1}"
|
||||||
|
progress = 0.2 + i*progress_step
|
||||||
|
progress_update(context, progress,
|
||||||
|
f"Processing {name_i}...")
|
||||||
|
progress_callback = progress_callback_factory(name_i,
|
||||||
|
progress,
|
||||||
|
progress_step)
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data[:, :, :, :, i:i+1],
|
||||||
|
name=name_i,
|
||||||
|
kind=kind)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layer.affine = affine
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
progress_update(context, 0.2,
|
||||||
|
f"Processing {name}...")
|
||||||
|
progress_callback = progress_callback_factory(name,
|
||||||
|
0.2,
|
||||||
|
0.7)
|
||||||
|
|
||||||
|
try:
|
||||||
|
layer = Layer(data=data,
|
||||||
|
name=name,
|
||||||
|
kind=kind)
|
||||||
|
|
||||||
|
layer.resize(shape=shape,
|
||||||
|
progress_callback=progress_callback)
|
||||||
|
|
||||||
|
layer.affine = affine
|
||||||
|
|
||||||
|
layers.append(layer)
|
||||||
|
except CancelledByUser:
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
self.has_error = e
|
||||||
|
return
|
||||||
|
|
||||||
|
if cancel():
|
||||||
|
return
|
||||||
|
|
||||||
|
self.layers = layers
|
||||||
|
progress_update(context, 0.9, "Creating Layers...")
|
||||||
|
|
||||||
|
self.is_cancelled = False
|
||||||
|
self.has_error = None
|
||||||
|
|
||||||
|
self.thread = threading.Thread(target=import_volumetric_data_func,
|
||||||
|
args=(self, context, lambda: self.is_cancelled))
|
||||||
|
|
||||||
|
self.thread.start()
|
||||||
|
self._timer = context.window_manager.event_timer_add(time_step=0.1,
|
||||||
|
window=context.window)
|
||||||
|
bpy.types.STATUSBAR_HT_header.append(progress_bar)
|
||||||
|
|
||||||
|
context.window_manager.modal_handler_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def modal(self, context, event):
|
||||||
|
if event.type == 'ESC':
|
||||||
|
self.is_cancelled = True
|
||||||
|
progress_update(context, 0.0, "Canceling...")
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
if event.type != 'TIMER':
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
bpy.context.workspace.status_text_set_internal(None)
|
||||||
|
if self.thread.is_alive():
|
||||||
|
return {'PASS_THROUGH'}
|
||||||
|
|
||||||
|
self.thread.join()
|
||||||
|
context.window_manager.event_timer_remove(self._timer)
|
||||||
|
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
||||||
|
progress_update(context, 1.0)
|
||||||
|
|
||||||
|
if self.is_cancelled:
|
||||||
|
self.report({"WARNING"}, "Canncelled by user.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
# Check if thread is cancelled by user
|
||||||
|
if self.has_error:
|
||||||
|
raise self.has_error
|
||||||
|
|
||||||
|
# Check if has return
|
||||||
|
if self.layers is None:
|
||||||
|
self.report({"ERROR"}, "Some thing went wrong.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
is_first_import = len(get_all_layer_objs()) == 0
|
||||||
|
|
||||||
|
if self.container_obj_name:
|
||||||
|
container_obj = bpy.data.objects.get(self.container_obj_name)
|
||||||
|
if container_obj is None:
|
||||||
|
raise Exception("Could not find target container")
|
||||||
|
|
||||||
|
container_obj = add_layers(self.layers,
|
||||||
|
container_obj=container_obj,
|
||||||
|
cache_dir=get_cache_dir())
|
||||||
|
else:
|
||||||
|
name = self.container_name or "Container"
|
||||||
|
container = Container(name=name,
|
||||||
|
layers=self.layers)
|
||||||
|
|
||||||
|
step_size = container.layers[0].bioxel_size[0]*5
|
||||||
|
container_obj = container_to_obj(container,
|
||||||
|
scene_scale=self.scene_scale,
|
||||||
|
step_size=step_size,
|
||||||
|
cache_dir=get_cache_dir())
|
||||||
|
|
||||||
|
select_object(container_obj)
|
||||||
|
|
||||||
|
# Change render setting for better result
|
||||||
|
if is_first_import:
|
||||||
|
bpy.ops.bioxelnodes.render_setting_preset('EXEC_DEFAULT',
|
||||||
|
preset="preview_c")
|
||||||
|
# bpy.ops.bioxelnodes.slice_viewer('EXEC_DEFAULT')
|
||||||
|
|
||||||
|
self.report({"INFO"}, "Successfully Imported")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
layer_kind = self.read_as.capitalize()
|
||||||
|
title = f"Add to **{self.container_obj_name}**, As {layer_kind}" \
|
||||||
|
if self.container_obj_name != "" else f"Init a Container, As {layer_kind}"
|
||||||
|
context.window_manager.invoke_props_dialog(self,
|
||||||
|
width=500,
|
||||||
|
title=title)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layer_shape = get_layer_shape(self.bioxel_size,
|
||||||
|
self.orig_shape,
|
||||||
|
self.orig_spacing)
|
||||||
|
|
||||||
|
orig_shape = tuple(self.orig_shape)
|
||||||
|
|
||||||
|
# change shape as sequence or not
|
||||||
|
channel_count = self.channel_count
|
||||||
|
frame_count = self.frame_count
|
||||||
|
if self.frame_source == "-1":
|
||||||
|
frame_count = 1
|
||||||
|
pass
|
||||||
|
elif self.frame_source == "0":
|
||||||
|
# frame as frame
|
||||||
|
pass
|
||||||
|
elif self.frame_source == "1":
|
||||||
|
frame_count = orig_shape[0]
|
||||||
|
layer_shape = (1, layer_shape[1], layer_shape[2])
|
||||||
|
elif self.frame_source == "2":
|
||||||
|
frame_count = orig_shape[1]
|
||||||
|
layer_shape = (layer_shape[0], 1, layer_shape[2])
|
||||||
|
elif self.frame_source == "3":
|
||||||
|
frame_count = orig_shape[2]
|
||||||
|
layer_shape = (layer_shape[0], layer_shape[1], 1)
|
||||||
|
else:
|
||||||
|
channel_count = 1
|
||||||
|
|
||||||
|
if self.read_as == "SCALAR":
|
||||||
|
layer_count = channel_count if self.split_channel else 1
|
||||||
|
channel_count = 1
|
||||||
|
elif self.read_as == "LABEL":
|
||||||
|
layer_count = self.label_count
|
||||||
|
channel_count = 1
|
||||||
|
else:
|
||||||
|
layer_count = 1
|
||||||
|
channel_count = 3
|
||||||
|
|
||||||
|
bioxel_count = layer_shape[0] * layer_shape[1] * layer_shape[2]
|
||||||
|
orig_shape_text = f"[{self.frame_count}, ({orig_shape[0]},{orig_shape[1]},{orig_shape[2]}), {self.channel_count}]"
|
||||||
|
layer_shape_text = f"{layer_count} x [{frame_count}, ({layer_shape[0]},{layer_shape[1]},{layer_shape[2]}), {channel_count}]"
|
||||||
|
|
||||||
|
if bioxel_count > 100000000:
|
||||||
|
layer_shape_text += "**TOO LARGE!**"
|
||||||
|
|
||||||
|
layer_size = get_layer_size(layer_shape,
|
||||||
|
self.bioxel_size,
|
||||||
|
self.scene_scale)
|
||||||
|
layer_size_text = f"Size will be: ({layer_size[0]:.2f}, {layer_size[1]:.2f}, {layer_size[2]:.2f}) m"
|
||||||
|
|
||||||
|
layout = self.layout
|
||||||
|
if self.container_obj_name == "":
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "container_name")
|
||||||
|
panel.prop(self, "scene_scale")
|
||||||
|
panel.label(text=layer_size_text)
|
||||||
|
|
||||||
|
panel = layout.box()
|
||||||
|
panel.prop(self, "layer_name")
|
||||||
|
panel.prop(self, "bioxel_size")
|
||||||
|
row = panel.row()
|
||||||
|
row.prop(self, "orig_spacing")
|
||||||
|
panel.prop(self, "frame_source")
|
||||||
|
|
||||||
|
if self.read_as == "SCALAR":
|
||||||
|
panel.prop(self, "split_channel",
|
||||||
|
text=f"Split Channel as Multi Layer")
|
||||||
|
|
||||||
|
panel.label(
|
||||||
|
text="Dimension Order: [Frame, (X-axis,Y-axis,Z-axis), Channel]")
|
||||||
|
panel.label(
|
||||||
|
text=f"Shape from {orig_shape_text} to {layer_shape_text}")
|
||||||
|
|
||||||
|
|
||||||
|
class ExportVolumetricData(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.export_volumetric_data"
|
||||||
|
bl_label = "Export Layer as VDB"
|
||||||
|
bl_description = "Export Layer as VDB"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(
|
||||||
|
subtype="FILE_PATH"
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
filename_ext = ".vdb"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
layer_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
return True if layer_obj else False
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.fileselect_add(self)
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
layer_obj = get_layer_obj(bpy.context.active_object)
|
||||||
|
|
||||||
|
filepath = f"{self.filepath.split('.')[0]}.vdb"
|
||||||
|
# "//"
|
||||||
|
source_dir = bpy.path.abspath(layer_obj.data.filepath)
|
||||||
|
|
||||||
|
output_path: Path = Path(filepath).resolve()
|
||||||
|
source_path: Path = Path(source_dir).resolve()
|
||||||
|
# print('output_path', output_path)
|
||||||
|
# print('source_path', source_path)
|
||||||
|
shutil.copy(source_path, output_path)
|
||||||
|
|
||||||
|
self.report({"INFO"}, f"Successfully exported to {output_path}")
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
604
bioxelnodes/operators/layer.py
Normal file
604
bioxelnodes/operators/layer.py
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..exceptions import NoContent
|
||||||
|
from ..bioxel.layer import Layer
|
||||||
|
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, set_layer_prop_value)
|
||||||
|
from ..bioxelutils.layer import layer_to_obj, obj_to_layer
|
||||||
|
from ..utils import get_cache_dir, copy_to_dir
|
||||||
|
|
||||||
|
|
||||||
|
def get_label_layer_selection(self, context):
|
||||||
|
items = [("None", "None", "")]
|
||||||
|
container_obj = get_container_obj(bpy.context.active_object)
|
||||||
|
|
||||||
|
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 execute(self, context):
|
||||||
|
if self.layer_obj == None:
|
||||||
|
self.report({"WARNING"}, "Get no layer.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
if is_missing_layer(self.layer_obj):
|
||||||
|
self.report({"WARNING"}, "Selected layer is lost.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
bpy.ops.bioxelnodes.add_node('EXEC_DEFAULT',
|
||||||
|
node_name="FetchLayer",
|
||||||
|
node_label="Fetch Layer")
|
||||||
|
node = bpy.context.active_node
|
||||||
|
|
||||||
|
node.inputs[0].default_value = self.layer_obj
|
||||||
|
node.label = get_layer_label(self.layer_obj)
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_selected_layers(context, layer_filter=None):
|
||||||
|
def _layer_filter(layer_obj, context):
|
||||||
|
return True
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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):
|
||||||
|
layer_obj = layer_to_obj(layer,
|
||||||
|
container_obj=context.object,
|
||||||
|
cache_dir=get_cache_dir())
|
||||||
|
|
||||||
|
bpy.ops.bioxelnodes.fetch_layer('INVOKE_DEFAULT',
|
||||||
|
layer_obj_name=layer_obj.name)
|
||||||
|
|
||||||
|
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'}
|
||||||
|
|
||||||
|
orig_layer = obj_to_layer(layer_obj)
|
||||||
|
try:
|
||||||
|
new_layer = self.operate(orig_layer, context)
|
||||||
|
except NoContent as e:
|
||||||
|
self.report({"WARNING"}, e.message)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
self.add_layer_node(context, new_layer)
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
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'}
|
||||||
|
else:
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
|
||||||
|
class RelocateLayer(bpy.types.Operator, LayerOperator):
|
||||||
|
bl_idname = "bioxelnodes.relocate_layer"
|
||||||
|
bl_label = "Relocate Layer Cache"
|
||||||
|
bl_description = "Relocate layer cache"
|
||||||
|
bl_icon = "FILE"
|
||||||
|
|
||||||
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
||||||
|
|
||||||
|
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 Iteration",
|
||||||
|
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",
|
||||||
|
default=True,
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
layer_obj = get_selected_layer(context)
|
||||||
|
if layer_obj:
|
||||||
|
scalar_min = get_layer_prop_value(layer_obj, "min")
|
||||||
|
self.fill_value = min(scalar_min, 0)
|
||||||
|
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,
|
||||||
|
) # 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, 3)
|
||||||
|
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, 3)
|
||||||
|
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 Iteration",
|
||||||
|
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, 3)
|
||||||
|
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'}
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveLayers(BatchLayerOperator):
|
||||||
|
fail_msg = "fail to remove."
|
||||||
|
|
||||||
|
def operate(self, layer_obj, context):
|
||||||
|
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)
|
||||||
|
|
||||||
|
cache_filepath = Path(bpy.path.abspath(
|
||||||
|
layer_obj.data.filepath)).resolve()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
context.window_manager.invoke_confirm(self,
|
||||||
|
event,
|
||||||
|
message=f"Are you sure to remove them?")
|
||||||
|
return {'RUNNING_MODAL'}
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
success_msg = "Successfully saved all selected layers."
|
||||||
|
|
||||||
|
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)
|
||||||
238
bioxelnodes/operators/misc.py
Normal file
238
bioxelnodes/operators/misc.py
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import bpy
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from ..bioxelutils.common import get_all_layer_objs, is_missing_layer, set_file_prop
|
||||||
|
from .layer import RemoveLayers, SaveLayersCache
|
||||||
|
|
||||||
|
from ..constants import NODE_LIB_FILEPATH, VERSION
|
||||||
|
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'}
|
||||||
|
|
||||||
|
node_lib_filename: bpy.props.StringProperty(
|
||||||
|
default=""
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
lib_filepath = Path(NODE_LIB_FILEPATH.parent,
|
||||||
|
f"{self.node_lib_filename}.blend")
|
||||||
|
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_filepath)
|
||||||
|
# FIXME: may cause crash
|
||||||
|
node_lib.reload()
|
||||||
|
|
||||||
|
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):
|
||||||
|
lib_dir = bpy.path.abspath(self.lib_dir)
|
||||||
|
local_lib_path: Path = Path(lib_dir, NODE_LIB_FILEPATH.name).resolve()
|
||||||
|
addon_lib_path: Path = NODE_LIB_FILEPATH
|
||||||
|
blend_path = Path(bpy.path.abspath("//")).resolve()
|
||||||
|
|
||||||
|
if local_lib_path != addon_lib_path:
|
||||||
|
shutil.copy(addon_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))
|
||||||
|
|
||||||
|
set_file_prop("addon_version", VERSION)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class CleanTemp(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.clear_temp"
|
||||||
|
bl_label = "Clean Temp"
|
||||||
|
bl_description = "Clean all cache in temp (include other project cache)"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cache_dir)
|
||||||
|
self.report({"INFO"}, f"Successfully cleaned temp.")
|
||||||
|
return {'FINISHED'}
|
||||||
|
except:
|
||||||
|
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'}
|
||||||
|
|
||||||
|
|
||||||
|
class RenderSettingPreset(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.render_setting_preset"
|
||||||
|
bl_label = "Render Setting Presets"
|
||||||
|
bl_description = "Render setting presets for bioxel"
|
||||||
|
bl_options = {'UNDO'}
|
||||||
|
|
||||||
|
PRESETS = {
|
||||||
|
"preview_e": "Preview (EEVEE)",
|
||||||
|
"preview_c": "Preview (Cycles)",
|
||||||
|
"production_e": "Production (EEVEE)",
|
||||||
|
"production_c": "Production (Cycles)"
|
||||||
|
}
|
||||||
|
|
||||||
|
preset: bpy.props.EnumProperty(name="Preset",
|
||||||
|
default="preview_c",
|
||||||
|
items=[(k, v, "")
|
||||||
|
for k, v in PRESETS.items()]) # type: ignore
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
if self.preset == "preview_e":
|
||||||
|
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||||
|
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||||
|
bpy.context.scene.eevee.taa_samples = 16
|
||||||
|
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||||
|
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_ray_depth = 16
|
||||||
|
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||||
|
|
||||||
|
elif self.preset == "production_e":
|
||||||
|
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||||
|
bpy.context.scene.eevee.use_taa_reprojection = False
|
||||||
|
bpy.context.scene.eevee.taa_samples = 16
|
||||||
|
bpy.context.scene.eevee.volumetric_tile_size = '1'
|
||||||
|
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_samples = 256
|
||||||
|
bpy.context.scene.eevee.volumetric_ray_depth = 16
|
||||||
|
bpy.context.scene.eevee.use_volumetric_shadows = True
|
||||||
|
|
||||||
|
elif self.preset == "preview_c":
|
||||||
|
bpy.context.scene.render.engine = 'CYCLES'
|
||||||
|
bpy.context.scene.cycles.shading_system = True
|
||||||
|
bpy.context.scene.cycles.volume_bounces = 12
|
||||||
|
bpy.context.scene.cycles.transparent_max_bounces = 16
|
||||||
|
bpy.context.scene.cycles.volume_preview_step_rate = 1
|
||||||
|
bpy.context.scene.cycles.volume_step_rate = 1
|
||||||
|
# bpy.context.scene.cycles.use_fast_gi = True
|
||||||
|
|
||||||
|
elif self.preset == "production_c":
|
||||||
|
bpy.context.scene.render.engine = 'CYCLES'
|
||||||
|
bpy.context.scene.cycles.shading_system = True
|
||||||
|
bpy.context.scene.cycles.volume_bounces = 16
|
||||||
|
bpy.context.scene.cycles.transparent_max_bounces = 32
|
||||||
|
bpy.context.scene.cycles.volume_preview_step_rate = 0.5
|
||||||
|
bpy.context.scene.cycles.volume_step_rate = 0.5
|
||||||
|
# bpy.context.scene.cycles.use_fast_gi = 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.taa_samples = 4
|
||||||
|
bpy.context.scene.eevee.volumetric_tile_size = '2'
|
||||||
|
bpy.context.scene.eevee.volumetric_shadow_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_samples = 128
|
||||||
|
bpy.context.scene.eevee.volumetric_ray_depth = 1
|
||||||
|
bpy.context.scene.eevee.use_volumetric_shadows = False
|
||||||
|
|
||||||
|
for area in bpy.context.screen.areas:
|
||||||
|
if area.type == 'VIEW_3D':
|
||||||
|
area.spaces[0].shading.type = 'MATERIAL'
|
||||||
|
area.spaces[0].shading.studio_light = 'studio.exr'
|
||||||
|
area.spaces[0].shading.studiolight_intensity = 2.5
|
||||||
|
area.spaces[0].shading.use_scene_lights = False
|
||||||
|
area.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'}
|
||||||
62
bioxelnodes/operators/node.py
Normal file
62
bioxelnodes/operators/node.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import bpy
|
||||||
|
|
||||||
|
from ..bioxelutils.common import get_file_prop, is_incompatible, local_lib_not_updated
|
||||||
|
from ..constants import VERSION
|
||||||
|
from ast import literal_eval
|
||||||
|
from ..bioxelutils.node import assign_node_group, get_node_group
|
||||||
|
from ..utils import get_use_link
|
||||||
|
|
||||||
|
|
||||||
|
class AddNode(bpy.types.Operator):
|
||||||
|
bl_idname = "bioxelnodes.add_node"
|
||||||
|
bl_label = "Add Node"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
node_name: bpy.props.StringProperty(
|
||||||
|
default='',
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
node_label: bpy.props.StringProperty(
|
||||||
|
default=''
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
node_description: bpy.props.StringProperty(
|
||||||
|
default="",
|
||||||
|
) # type: ignore
|
||||||
|
|
||||||
|
@property
|
||||||
|
def node_type(self):
|
||||||
|
return f"BioxelNodes_{self.node_name}"
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
space = context.space_data
|
||||||
|
if space.type != "NODE_EDITOR":
|
||||||
|
self.report({"ERROR"}, "Not in node editor.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if not space.edit_tree.is_editable:
|
||||||
|
self.report({"ERROR"}, "Not editable.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
if is_incompatible():
|
||||||
|
self.report({"ERROR"},
|
||||||
|
"Current addon verison is not compatible to this file. If you insist on editing this file please keep the same addon version.")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
get_node_group(self.node_type, get_use_link())
|
||||||
|
bpy.ops.node.add_node(
|
||||||
|
'INVOKE_DEFAULT',
|
||||||
|
type='GeometryNodeGroup',
|
||||||
|
use_transform=True
|
||||||
|
)
|
||||||
|
node = bpy.context.active_node
|
||||||
|
assign_node_group(node, self.node_type)
|
||||||
|
node.label = self.node_label
|
||||||
|
|
||||||
|
node.show_options = False
|
||||||
|
|
||||||
|
if local_lib_not_updated():
|
||||||
|
self.report({"WARNING"},
|
||||||
|
"Local library version does not match the current addon version, which may cause problems, please save the node library again.")
|
||||||
|
|
||||||
|
return {"FINISHED"}
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .exceptions import CancelledByUser
|
|
||||||
from .utils import get_text_index_str
|
|
||||||
|
|
||||||
try:
|
|
||||||
import SimpleITK as sitk
|
|
||||||
from pyometiff import OMETIFFReader
|
|
||||||
import mrcfile
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
"""
|
|
||||||
Convert any volumetric data to 3D numpy array with order TXYZC
|
|
||||||
"""
|
|
||||||
|
|
||||||
SUPPORT_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
|
||||||
'.bmp', '.BMP',
|
|
||||||
'.PIC', '.pic',
|
|
||||||
'.gipl', '.gipl.gz',
|
|
||||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
|
||||||
'.lsm', '.LSM',
|
|
||||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
|
||||||
'.mnc', '.MNC',
|
|
||||||
'.mrc', '.rec',
|
|
||||||
'.mha', '.mhd',
|
|
||||||
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
|
||||||
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
|
||||||
'.nrrd', '.nhdr',
|
|
||||||
'.png', '.PNG',
|
|
||||||
'.vtk',
|
|
||||||
'.ome.tiff', '.ome.tif',
|
|
||||||
'.mrc', '.mrc.gz', '.map', '.map.gz']
|
|
||||||
|
|
||||||
OME_EXTS = ['.ome.tiff', '.ome.tif',
|
|
||||||
'.tif', '.TIF', '.tiff', '.TIFF']
|
|
||||||
|
|
||||||
MRC_EXTS = ['.mrc', '.mrc.gz', '.map', '.map.gz']
|
|
||||||
|
|
||||||
SEQUENCE_EXTS = ['.bmp', '.BMP',
|
|
||||||
'.jpg', '.JPG', '.jpeg', '.JPEG',
|
|
||||||
'.tif', '.TIF', '.tiff', '.TIFF',
|
|
||||||
'.png', '.PNG']
|
|
||||||
|
|
||||||
DICOM_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA']
|
|
||||||
|
|
||||||
FH_EXTS = ['', '.dcm', '.DCM', '.DICOM', '.ima', '.IMA',
|
|
||||||
'.gipl', '.gipl.gz',
|
|
||||||
'.mnc', '.MNC',
|
|
||||||
'.mrc', '.rec',
|
|
||||||
'.mha', '.mhd',
|
|
||||||
'.nia', '.nii', '.nii.gz', '.hdr', '.img', '.img.gz',
|
|
||||||
'.hdf', '.h4', '.hdf4', '.he2', '.h5', '.hdf5', '.he5',
|
|
||||||
'.nrrd', '.nhdr',
|
|
||||||
'.vtk',
|
|
||||||
'.gz']
|
|
||||||
|
|
||||||
|
|
||||||
def get_ext(filepath: str) -> str:
|
|
||||||
file_path = Path(filepath)
|
|
||||||
if file_path.name.endswith(".nii.gz"):
|
|
||||||
return ".nii.gz"
|
|
||||||
elif file_path.name.endswith(".img.gz"):
|
|
||||||
return ".img.gz"
|
|
||||||
elif file_path.name.endswith(".gipl.gz"):
|
|
||||||
return ".gipl.gz"
|
|
||||||
elif file_path.name.endswith(".ome.tiff"):
|
|
||||||
return ".ome.tiff"
|
|
||||||
elif file_path.name.endswith(".ome.tif"):
|
|
||||||
return ".ome.tif"
|
|
||||||
elif file_path.name.endswith(".mrc.gz"):
|
|
||||||
return ".mrc.gz"
|
|
||||||
elif file_path.name.endswith(".map.gz"):
|
|
||||||
return ".map.gz"
|
|
||||||
else:
|
|
||||||
return file_path.suffix
|
|
||||||
|
|
||||||
|
|
||||||
def get_sequence_name(filepath: str) -> str:
|
|
||||||
ext = get_ext(filepath)
|
|
||||||
filename = Path(filepath).name.removesuffix(ext)
|
|
||||||
index: str = get_text_index_str(filename)
|
|
||||||
return filename.removesuffix(index)
|
|
||||||
|
|
||||||
|
|
||||||
def get_sequence_index(filepath: str) -> int:
|
|
||||||
ext = get_ext(filepath)
|
|
||||||
filename = Path(filepath).name.removesuffix(ext)
|
|
||||||
index: str = get_text_index_str(filename)
|
|
||||||
return int(index) if index else 0
|
|
||||||
|
|
||||||
|
|
||||||
def collect_sequence(filepath: str):
|
|
||||||
file_path = Path(filepath).resolve()
|
|
||||||
|
|
||||||
files = list(file_path.parent.iterdir())
|
|
||||||
file_dict = {}
|
|
||||||
for file in files:
|
|
||||||
if file.is_file() \
|
|
||||||
and get_ext(file_path) == get_ext(file) \
|
|
||||||
and get_sequence_name(file_path) == get_sequence_name(file):
|
|
||||||
file_dict[get_sequence_index(file)] = file
|
|
||||||
|
|
||||||
for key in file_dict.copy().keys():
|
|
||||||
if not file_dict.get(key+1) \
|
|
||||||
and not file_dict.get(key-1):
|
|
||||||
del file_dict[key]
|
|
||||||
|
|
||||||
file_dict = dict(sorted(file_dict.items()))
|
|
||||||
sequence = [str(f) for f in file_dict.values()]
|
|
||||||
|
|
||||||
if len(sequence) == 0:
|
|
||||||
sequence = [str(file_path)]
|
|
||||||
|
|
||||||
return sequence
|
|
||||||
|
|
||||||
|
|
||||||
def parse_volumetric_data(filepath: str, series_id="", progress_callback=None):
|
|
||||||
"""Parse any volumetric data to numpy with shap (T,X,Y,Z,C)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filepath (str): file path
|
|
||||||
series_id (str, optional): DICOM series id. Defaults to "".
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_type_: _description_
|
|
||||||
"""
|
|
||||||
ext = get_ext(filepath)
|
|
||||||
|
|
||||||
if progress_callback:
|
|
||||||
progressing = progress_callback(0, "Reading the Data...")
|
|
||||||
if not progressing:
|
|
||||||
raise CancelledByUser
|
|
||||||
|
|
||||||
is_sequence = False
|
|
||||||
if ext in SEQUENCE_EXTS:
|
|
||||||
sequence = collect_sequence(filepath)
|
|
||||||
if len(sequence) > 1:
|
|
||||||
is_sequence = True
|
|
||||||
|
|
||||||
volume = None
|
|
||||||
|
|
||||||
# Parsing with mrcfile
|
|
||||||
if volume is None and ext in MRC_EXTS and not is_sequence:
|
|
||||||
print("Parsing with mrcfile...")
|
|
||||||
# TODO: much to do with mrc
|
|
||||||
with mrcfile.open(filepath) as mrc:
|
|
||||||
volume = mrc.data
|
|
||||||
# mrc.print_header()
|
|
||||||
# print(volume.shape)
|
|
||||||
# print(mrc.voxel_size)
|
|
||||||
|
|
||||||
if mrc.is_single_image():
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
elif mrc.is_image_stack():
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
elif mrc.is_volume():
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
elif mrc.is_volume_stack():
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
|
||||||
shape = volume.shape[1:4]
|
|
||||||
spacing = (mrc.voxel_size.x, mrc.voxel_size.y, mrc.voxel_size.z)
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"description": "",
|
|
||||||
"shape": shape,
|
|
||||||
"spacing": spacing,
|
|
||||||
"origin": (0, 0, 0),
|
|
||||||
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
|
|
||||||
"frame_count": volume.shape[0],
|
|
||||||
"channel_count": volume.shape[-1],
|
|
||||||
"is_oriented": False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Parsing with OMETIFFReader
|
|
||||||
if volume is None and ext in OME_EXTS and not is_sequence:
|
|
||||||
print("Parsing with OMETIFFReader...")
|
|
||||||
reader = OMETIFFReader(fpath=filepath)
|
|
||||||
ome_volume, metadata, xml_metadata = reader.read()
|
|
||||||
|
|
||||||
if progress_callback:
|
|
||||||
progressing = progress_callback(0.5, "Transpose to 'TXYZC'...")
|
|
||||||
if not progressing:
|
|
||||||
raise CancelledByUser
|
|
||||||
|
|
||||||
try:
|
|
||||||
# print(ome_volume.shape)
|
|
||||||
# for key in metadata:
|
|
||||||
# print(f"{key},{metadata[key]}")
|
|
||||||
ome_order = metadata['DimOrder BF Array']
|
|
||||||
if ome_volume.ndim == 2:
|
|
||||||
ome_order = ome_order.replace("T", "")\
|
|
||||||
.replace("C", "").replace("Z", "")
|
|
||||||
bioxel_order = (ome_order.index('X'),
|
|
||||||
ome_order.index('Y'))
|
|
||||||
volume = np.transpose(ome_volume, bioxel_order)
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
elif ome_volume.ndim == 3:
|
|
||||||
# -> XYZC
|
|
||||||
ome_order = ome_order.replace("T", "").replace("C", "")
|
|
||||||
bioxel_order = (ome_order.index('X'),
|
|
||||||
ome_order.index('Y'),
|
|
||||||
ome_order.index('Z'))
|
|
||||||
volume = np.transpose(ome_volume, bioxel_order)
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
elif ome_volume.ndim == 4:
|
|
||||||
# -> XYZC
|
|
||||||
ome_order = ome_order.replace("T", "")
|
|
||||||
bioxel_order = (ome_order.index('X'),
|
|
||||||
ome_order.index('Y'),
|
|
||||||
ome_order.index('Z'),
|
|
||||||
ome_order.index('C'))
|
|
||||||
volume = np.transpose(ome_volume, bioxel_order)
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
elif ome_volume.ndim == 5:
|
|
||||||
# -> TXYZC
|
|
||||||
bioxel_order = (ome_order.index('T'),
|
|
||||||
ome_order.index('X'),
|
|
||||||
ome_order.index('Y'),
|
|
||||||
ome_order.index('Z'),
|
|
||||||
ome_order.index('C'))
|
|
||||||
volume = np.transpose(ome_volume, bioxel_order)
|
|
||||||
|
|
||||||
shape = volume.shape[1:4]
|
|
||||||
|
|
||||||
try:
|
|
||||||
spacing = (metadata['PhysicalSizeX'],
|
|
||||||
metadata['PhysicalSizeY'],
|
|
||||||
metadata['PhysicalSizeZ'])
|
|
||||||
except:
|
|
||||||
spacing = (1, 1, 1)
|
|
||||||
|
|
||||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"description": "",
|
|
||||||
"shape": shape,
|
|
||||||
"spacing": spacing,
|
|
||||||
"origin": (0, 0, 0),
|
|
||||||
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
|
|
||||||
"frame_count": volume.shape[0],
|
|
||||||
"channel_count": volume.shape[-1],
|
|
||||||
"is_oriented": False
|
|
||||||
}
|
|
||||||
except:
|
|
||||||
...
|
|
||||||
|
|
||||||
# Parsing with SimpleITK
|
|
||||||
if volume is None:
|
|
||||||
print("Parsing with SimpleITK...")
|
|
||||||
if ext in DICOM_EXTS:
|
|
||||||
dir_path = Path(filepath).resolve().parent
|
|
||||||
reader = sitk.ImageSeriesReader()
|
|
||||||
reader.MetaDataDictionaryArrayUpdateOn()
|
|
||||||
reader.LoadPrivateTagsOn()
|
|
||||||
series_files = reader.GetGDCMSeriesFileNames(
|
|
||||||
str(dir_path), series_id)
|
|
||||||
reader.SetFileNames(series_files)
|
|
||||||
|
|
||||||
itk_volume = reader.Execute()
|
|
||||||
# for k in reader.GetMetaDataKeys(0):
|
|
||||||
# v = reader.GetMetaData(0, k)
|
|
||||||
# print(f'({k}) = = "{v}"')
|
|
||||||
|
|
||||||
def get_meta(key):
|
|
||||||
try:
|
|
||||||
stirng = reader.GetMetaData(0, key).removesuffix(" ")
|
|
||||||
if stirng in ["No study description",
|
|
||||||
"No series description"]:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return stirng
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
study_description = get_meta("0008|1030")
|
|
||||||
series_description = get_meta("0008|103e")
|
|
||||||
series_modality = get_meta("0008|0060")
|
|
||||||
|
|
||||||
name = study_description or dir_path.name
|
|
||||||
if series_description and series_modality:
|
|
||||||
description = f"{series_description}-{series_modality}"
|
|
||||||
elif series_description:
|
|
||||||
description = series_description
|
|
||||||
elif series_modality:
|
|
||||||
description = series_modality
|
|
||||||
else:
|
|
||||||
description = ""
|
|
||||||
|
|
||||||
name = name.replace(" ", "-")
|
|
||||||
description = description.replace(" ", "-")
|
|
||||||
|
|
||||||
elif ext in SEQUENCE_EXTS and is_sequence:
|
|
||||||
try:
|
|
||||||
itk_volume = sitk.ReadImage(sequence)
|
|
||||||
name = get_sequence_name(filepath).replace(" ", "-")
|
|
||||||
description = ""
|
|
||||||
except RuntimeError as e:
|
|
||||||
raise e
|
|
||||||
else:
|
|
||||||
itk_volume = sitk.ReadImage(filepath)
|
|
||||||
name = Path(filepath).name.removesuffix(ext).replace(" ", "-")
|
|
||||||
description = ""
|
|
||||||
|
|
||||||
# for key in itk_volume.GetMetaDataKeys():
|
|
||||||
# print(f"{key},{itk_volume.GetMetaData(key)}")
|
|
||||||
|
|
||||||
if progress_callback:
|
|
||||||
progressing = progress_callback(0.5, "Transpose to 'TXYZC'...")
|
|
||||||
if not progressing:
|
|
||||||
raise CancelledByUser
|
|
||||||
|
|
||||||
if itk_volume.GetDimension() == 2:
|
|
||||||
volume = sitk.GetArrayFromImage(itk_volume)
|
|
||||||
|
|
||||||
if volume.ndim == 3:
|
|
||||||
volume = np.transpose(volume, (1, 0, 2))
|
|
||||||
|
|
||||||
volume = np.expand_dims(volume, axis=-2) # expend Z
|
|
||||||
else:
|
|
||||||
volume = np.transpose(volume)
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend Z
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"shape": volume.shape[1:4],
|
|
||||||
"spacing": (1, 1, 1),
|
|
||||||
"origin": (0, 0, 0),
|
|
||||||
"direction": (1, 0, 0, 0, 1, 0, 0, 0, 1),
|
|
||||||
"frame_count": 1,
|
|
||||||
"channel_count": volume.shape[-1],
|
|
||||||
"is_oriented": False
|
|
||||||
}
|
|
||||||
|
|
||||||
elif itk_volume.GetDimension() == 3:
|
|
||||||
itk_volume = sitk.DICOMOrient(itk_volume, 'RAS')
|
|
||||||
|
|
||||||
volume = sitk.GetArrayFromImage(itk_volume)
|
|
||||||
|
|
||||||
# transpose ijk to kji
|
|
||||||
if volume.ndim == 4:
|
|
||||||
volume = np.transpose(volume, (2, 1, 0, 3))
|
|
||||||
else:
|
|
||||||
volume = np.transpose(volume)
|
|
||||||
volume = np.expand_dims(volume, axis=-1) # expend channel
|
|
||||||
|
|
||||||
volume = np.expand_dims(volume, axis=0) # expend frame
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"shape": tuple(itk_volume.GetSize()),
|
|
||||||
"spacing": tuple(itk_volume.GetSpacing()),
|
|
||||||
"origin": tuple(itk_volume.GetOrigin()),
|
|
||||||
"direction": tuple(itk_volume.GetDirection()),
|
|
||||||
"frame_count": 1,
|
|
||||||
"channel_count": volume.shape[-1],
|
|
||||||
"is_oriented": True
|
|
||||||
}
|
|
||||||
|
|
||||||
elif itk_volume.GetDimension() == 4:
|
|
||||||
# FIXME: not sure...
|
|
||||||
direction = np.array(itk_volume.GetDirection())
|
|
||||||
direction = direction.reshape(3, 3) if itk_volume.GetDimension() == 3 \
|
|
||||||
else direction.reshape(4, 4)
|
|
||||||
|
|
||||||
direction = direction[1:, 1:]
|
|
||||||
direction = tuple(direction.flatten())
|
|
||||||
|
|
||||||
volume = sitk.GetArrayFromImage(itk_volume)
|
|
||||||
|
|
||||||
if volume.ndim == 5:
|
|
||||||
volume = np.transpose(volume, (0, 3, 2, 1, 4))
|
|
||||||
else:
|
|
||||||
volume = np.transpose(volume, (0, 3, 2, 1))
|
|
||||||
volume = np.expand_dims(volume, axis=-1)
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"shape": tuple(itk_volume.GetSize()[:3]),
|
|
||||||
"spacing": tuple(itk_volume.GetSpacing()[:3]),
|
|
||||||
"origin": tuple(itk_volume.GetOrigin()[:3]),
|
|
||||||
"direction": direction,
|
|
||||||
"frame_count": volume.shape[0],
|
|
||||||
"channel_count": volume.shape[-1],
|
|
||||||
"is_oriented": False
|
|
||||||
}
|
|
||||||
|
|
||||||
return volume, meta
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from .externalpackage import ExternalPackagePreferences
|
|
||||||
|
|
||||||
|
|
||||||
class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferences):
|
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
||||||
bl_idname = __package__
|
bl_idname = __package__
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
cache_dir: bpy.props.StringProperty(
|
||||||
@@ -12,18 +11,19 @@ class BioxelNodesPreferences(bpy.types.AddonPreferences, ExternalPackagePreferen
|
|||||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
default=str(Path(Path.home(), '.bioxelnodes'))
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
|
|
||||||
do_change_render_setting: bpy.props.BoolProperty(
|
change_render_setting: bpy.props.BoolProperty(
|
||||||
name="Change Render Setting",
|
name="Change Render Setting on First Import",
|
||||||
default=True,
|
default=True,
|
||||||
) # type: ignore
|
) # 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):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
|
|
||||||
# ExternalPackagePreferences Config
|
|
||||||
self.requirements_dir = str(Path(__file__).parent)
|
|
||||||
super().draw(context)
|
|
||||||
|
|
||||||
layout.label(text="Configuration")
|
layout.label(text="Configuration")
|
||||||
layout.prop(self, 'cache_dir')
|
layout.prop(self, 'cache_dir')
|
||||||
layout.prop(self, "do_change_render_setting")
|
layout.prop(self, "change_render_setting")
|
||||||
|
layout.prop(self, "node_import_method")
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from .bioxelutils.common import get_node_type
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class BIOXELNODES_Series(bpy.types.PropertyGroup):
|
class BIOXELNODES_Series(bpy.types.PropertyGroup):
|
||||||
id: bpy.props.StringProperty() # type: ignore
|
id: bpy.props.StringProperty() # type: ignore
|
||||||
label: bpy.props.StringProperty() # type: ignore
|
label: bpy.props.StringProperty() # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
import re
|
|
||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
from .utils import copy_to_dir, get_all_layers, get_container, get_container_from_selection, get_container_layers
|
|
||||||
from .nodes import custom_nodes
|
|
||||||
|
|
||||||
CLASS_PREFIX = "BIOXELNODES_MT_NODES"
|
|
||||||
|
|
||||||
|
|
||||||
class ReLinkNodes(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.relink_nodes"
|
|
||||||
bl_label = "Relink Nodes to Addon"
|
|
||||||
bl_description = "Relink all nodes to addon source"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
file_name = Path(custom_nodes.nodes_file).name
|
|
||||||
for lib in bpy.data.libraries:
|
|
||||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
|
||||||
lib_name = lib_path.name
|
|
||||||
if lib_name == file_name:
|
|
||||||
if str(lib_path) != custom_nodes.nodes_file:
|
|
||||||
lib.filepath = custom_nodes.nodes_file
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully relinked.")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class SaveStagedData(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.save_staged_data"
|
|
||||||
bl_label = "Save Staged Data"
|
|
||||||
bl_description = "Save all staged data in this file for sharing"
|
|
||||||
|
|
||||||
save_layer: bpy.props.BoolProperty(
|
|
||||||
name="Save Layer VDB Cache",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
|
||||||
name="Cache Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default="//"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
save_lib: bpy.props.BoolProperty(
|
|
||||||
name="Save Node Library File",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
lib_dir: bpy.props.StringProperty(
|
|
||||||
name="Library Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default="//"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
if self.save_lib:
|
|
||||||
files = []
|
|
||||||
for classname in dir(bpy.types):
|
|
||||||
if CLASS_PREFIX in classname:
|
|
||||||
cls = getattr(bpy.types, classname)
|
|
||||||
files.append(cls.nodes_file)
|
|
||||||
files = list(set(files))
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
file_name = Path(file).name
|
|
||||||
# "//"
|
|
||||||
lib_dir = bpy.path.abspath(self.lib_dir)
|
|
||||||
|
|
||||||
output_path: Path = Path(lib_dir, file_name).resolve()
|
|
||||||
source_path: Path = Path(file).resolve()
|
|
||||||
|
|
||||||
if output_path != source_path:
|
|
||||||
shutil.copy(source_path, output_path)
|
|
||||||
|
|
||||||
for lib in bpy.data.libraries:
|
|
||||||
lib_path = Path(bpy.path.abspath(lib.filepath)).resolve()
|
|
||||||
if lib_path == source_path:
|
|
||||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
|
||||||
lib.filepath = bpy.path.relpath(
|
|
||||||
str(output_path), start=str(blend_path))
|
|
||||||
|
|
||||||
self.report({"INFO"}, f"Successfully saved to {output_path}")
|
|
||||||
|
|
||||||
if self.save_layer:
|
|
||||||
fails = []
|
|
||||||
for layer in get_all_layers():
|
|
||||||
try:
|
|
||||||
save_layer(layer, self.cache_dir)
|
|
||||||
except:
|
|
||||||
fails.append(layer)
|
|
||||||
|
|
||||||
if len(fails) == 0:
|
|
||||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
|
||||||
else:
|
|
||||||
self.report(
|
|
||||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.invoke_props_dialog(self,
|
|
||||||
width=500)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
return bpy.data.is_saved
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "save_layer")
|
|
||||||
panel.prop(self, "cache_dir")
|
|
||||||
panel = layout.box()
|
|
||||||
panel.prop(self, "save_lib")
|
|
||||||
panel.prop(self, "lib_dir")
|
|
||||||
|
|
||||||
|
|
||||||
def save_layer(layer, output_dir):
|
|
||||||
|
|
||||||
pattern = r'\.\d{4}\.'
|
|
||||||
|
|
||||||
# "//"
|
|
||||||
output_dir = bpy.path.abspath(output_dir)
|
|
||||||
source_dir = bpy.path.abspath(layer.data.filepath)
|
|
||||||
|
|
||||||
source_path: Path = Path(source_dir).resolve()
|
|
||||||
is_sequence = re.search(pattern, source_path.name) is not None
|
|
||||||
name = layer.name if is_sequence else f"{layer.name}.vdb"
|
|
||||||
output_path: Path = Path(output_dir, name, source_path.name).resolve() \
|
|
||||||
if is_sequence else Path(output_dir, name).resolve()
|
|
||||||
|
|
||||||
if output_path != source_path:
|
|
||||||
copy_to_dir(source_path.parent if is_sequence else source_path,
|
|
||||||
output_path.parent.parent if is_sequence else output_path.parent,
|
|
||||||
new_name=name)
|
|
||||||
|
|
||||||
blend_path = Path(bpy.path.abspath("//")).resolve()
|
|
||||||
|
|
||||||
layer.data.filepath = bpy.path.relpath(
|
|
||||||
str(output_path), start=str(blend_path))
|
|
||||||
|
|
||||||
|
|
||||||
class SaveLayers(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.save_layers"
|
|
||||||
bl_label = "Save Layers"
|
|
||||||
bl_description = "Save Container Layers to Directory."
|
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
|
||||||
name="Layer Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default="//"
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def poll(cls, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
return len(containers) > 0
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
containers = get_container_from_selection()
|
|
||||||
|
|
||||||
if len(containers) == 0:
|
|
||||||
self.report({"WARNING"}, "Cannot find any bioxel container.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
fails = []
|
|
||||||
for container in containers:
|
|
||||||
for layer in get_container_layers(container):
|
|
||||||
try:
|
|
||||||
save_layer(layer, self.cache_dir)
|
|
||||||
except:
|
|
||||||
fails.append(layer)
|
|
||||||
|
|
||||||
if len(fails) == 0:
|
|
||||||
self.report({"INFO"}, f"Successfully saved bioxel layers.")
|
|
||||||
else:
|
|
||||||
self.report(
|
|
||||||
{"WARNING"}, f"{','.join([layer.name for layer in fails])} fail to save.")
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.invoke_props_dialog(self,
|
|
||||||
width=500)
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
|
|
||||||
|
|
||||||
class CleanAllCaches(bpy.types.Operator):
|
|
||||||
bl_idname = "bioxelnodes.clear_all_caches"
|
|
||||||
bl_label = "Clean All Caches in Temp"
|
|
||||||
bl_description = "Clean all caches saved in temp"
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
try:
|
|
||||||
shutil.rmtree(cache_dir)
|
|
||||||
self.report({"INFO"}, f"Successfully cleaned caches.")
|
|
||||||
return {'FINISHED'}
|
|
||||||
except:
|
|
||||||
self.report({"WARNING"},
|
|
||||||
"Fail to clean caches, you may do it manually.")
|
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
def invoke(self, context, event):
|
|
||||||
context.window_manager.invoke_confirm(self,
|
|
||||||
event,
|
|
||||||
message="All caches will be cleaned, include other project files, do you still want to clean?")
|
|
||||||
return {'RUNNING_MODAL'}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from ._interpolation import zoom
|
|
||||||
from ._filters import gaussian_filter, median_filter
|
|
||||||
Binary file not shown.
@@ -1,17 +1,6 @@
|
|||||||
import bpy
|
|
||||||
import mathutils
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import pyopenvdb as vdb
|
|
||||||
from uuid import uuid4
|
|
||||||
import shutil
|
import shutil
|
||||||
|
import bpy
|
||||||
|
|
||||||
def select_object(target_obj):
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
obj.select_set(False)
|
|
||||||
|
|
||||||
target_obj.select_set(True)
|
|
||||||
bpy.context.view_layer.objects.active = target_obj
|
|
||||||
|
|
||||||
|
|
||||||
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
||||||
@@ -44,32 +33,12 @@ def copy_to_dir(source_path, dir_path, new_name=None, exist_ok=True):
|
|||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
|
|
||||||
def move_node_to_node(node, target_node, offset=(0, 0)):
|
def select_object(target_obj):
|
||||||
node.location.x = target_node.location.x + offset[0]
|
for obj in bpy.data.objects:
|
||||||
node.location.y = target_node.location.y + offset[1]
|
obj.select_set(False)
|
||||||
|
|
||||||
|
target_obj.select_set(True)
|
||||||
def move_node_between_nodes(node, target_nodes, offset=(0, 0)):
|
bpy.context.view_layer.objects.active = target_obj
|
||||||
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 progress_bar(self, context):
|
def progress_bar(self, context):
|
||||||
@@ -87,208 +56,17 @@ def progress_update(context, factor, text=""):
|
|||||||
context.window_manager.bioxelnodes_progress_text = text
|
context.window_manager.bioxelnodes_progress_text = text
|
||||||
|
|
||||||
|
|
||||||
def calc_bbox_verts(origin, size):
|
def get_preferences():
|
||||||
bbox_origin = mathutils.Vector(
|
return bpy.context.preferences.addons[__package__].preferences
|
||||||
(origin[0], origin[1], origin[2]))
|
|
||||||
bbox_size = mathutils.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 lock_transform(obj):
|
def get_cache_dir():
|
||||||
obj.lock_location[0] = True
|
preferences = get_preferences()
|
||||||
obj.lock_location[1] = True
|
cache_path = Path(preferences.cache_dir, 'VDBs')
|
||||||
obj.lock_location[2] = True
|
cache_path.mkdir(parents=True, exist_ok=True)
|
||||||
obj.lock_rotation[0] = True
|
return str(cache_path)
|
||||||
obj.lock_rotation[1] = True
|
|
||||||
obj.lock_rotation[2] = True
|
|
||||||
obj.lock_scale[0] = True
|
|
||||||
obj.lock_scale[1] = True
|
|
||||||
obj.lock_scale[2] = True
|
|
||||||
|
|
||||||
|
|
||||||
def hide_in_ray(obj):
|
def get_use_link():
|
||||||
obj.visible_camera = False
|
preferences = get_preferences()
|
||||||
obj.visible_diffuse = False
|
return preferences.node_import_method == "LINK"
|
||||||
obj.visible_glossy = False
|
|
||||||
obj.visible_transmission = False
|
|
||||||
obj.visible_volume_scatter = False
|
|
||||||
obj.visible_shadow = False
|
|
||||||
|
|
||||||
|
|
||||||
def save_vdb(grids, context):
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
vdb_path = Path(cache_dir, f"{uuid4()}.vdb")
|
|
||||||
# print(f"Storing the VDB file ({str(vdb_path)})...")
|
|
||||||
vdb.write(str(vdb_path), grids=grids)
|
|
||||||
|
|
||||||
return vdb_path
|
|
||||||
|
|
||||||
|
|
||||||
def save_vdbs(grids_sequence, context):
|
|
||||||
preferences = context.preferences.addons[__package__].preferences
|
|
||||||
cache_dir = Path(preferences.cache_dir, 'VDBs')
|
|
||||||
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
vdb_name = str(uuid4())
|
|
||||||
vdb_dir_path = Path(cache_dir, vdb_name)
|
|
||||||
vdb_dir_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
vdb_paths = []
|
|
||||||
for f, grids in enumerate(grids_sequence):
|
|
||||||
vdb_path = Path(vdb_dir_path, f"{vdb_name}.{str(f+1).zfill(4)}.vdb")
|
|
||||||
# print(f"Storing the VDB file ({str(vdb_path)})...")
|
|
||||||
vdb.write(str(vdb_path), grids=grids)
|
|
||||||
vdb_paths.append(vdb_path)
|
|
||||||
|
|
||||||
return vdb_paths
|
|
||||||
|
|
||||||
|
|
||||||
def get_container_from_selection():
|
|
||||||
containers = []
|
|
||||||
for obj in bpy.context.selected_objects:
|
|
||||||
if get_container(obj):
|
|
||||||
containers.append(obj)
|
|
||||||
# if bpy.context.active_object:
|
|
||||||
# if bpy.context.active_object.get('bioxel_layer'):
|
|
||||||
# if bpy.context.active_object.parent.get('bioxel_container'):
|
|
||||||
# containers.append(bpy.context.active_object.parent)
|
|
||||||
|
|
||||||
return list(set(containers))
|
|
||||||
|
|
||||||
|
|
||||||
def get_container(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(current_obj):
|
|
||||||
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_layers(container):
|
|
||||||
layers = []
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if obj.parent == container and get_layer(obj):
|
|
||||||
layers.append(obj)
|
|
||||||
|
|
||||||
return layers
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_layers():
|
|
||||||
layers = []
|
|
||||||
for obj in bpy.data.objects:
|
|
||||||
if get_layer(obj):
|
|
||||||
layers.append(obj)
|
|
||||||
|
|
||||||
return layers
|
|
||||||
|
|
||||||
|
|
||||||
def get_text_index_str(text):
|
|
||||||
# Initialize an empty string to store the digits
|
|
||||||
digits = ""
|
|
||||||
|
|
||||||
# Iterate through the characters in reverse order
|
|
||||||
started = False
|
|
||||||
for char in text[::-1]:
|
|
||||||
if char.isdigit():
|
|
||||||
started = True
|
|
||||||
# If the character is a digit, add it to the digits string
|
|
||||||
digits += char
|
|
||||||
else:
|
|
||||||
if started:
|
|
||||||
# If a non-digit character is encountered, stop the loop
|
|
||||||
break
|
|
||||||
|
|
||||||
# Reverse the digits string to get the correct order
|
|
||||||
last_number = digits[::-1]
|
|
||||||
|
|
||||||
return last_number
|
|
||||||
|
|
||||||
|
|
||||||
def add_driver(target_prop, var_sources, expression):
|
|
||||||
|
|
||||||
driver = target_prop.driver_add("default_value")
|
|
||||||
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):
|
|
||||||
target_prop = target.inputs.get(target_prop)
|
|
||||||
drivers = [
|
|
||||||
{
|
|
||||||
"source": source,
|
|
||||||
"prop": source_prop
|
|
||||||
}
|
|
||||||
]
|
|
||||||
expression = "var0"
|
|
||||||
add_driver(target_prop, drivers, expression)
|
|
||||||
|
|||||||
96
build.py
Normal file
96
build.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import tomlkit
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Platform:
|
||||||
|
pypi_suffix: str
|
||||||
|
blender_tag: str
|
||||||
|
|
||||||
|
|
||||||
|
required_packages = ["SimpleITK==2.3.1",
|
||||||
|
"pyometiff==1.0.0",
|
||||||
|
"mrcfile==1.5.1",
|
||||||
|
"h5py==3.11.0",
|
||||||
|
"transforms3d==0.4.2",
|
||||||
|
"tifffile==2024.7.24"]
|
||||||
|
|
||||||
|
|
||||||
|
platforms = {"windows-x64": Platform(pypi_suffix="win_amd64",
|
||||||
|
blender_tag="windows-x64"),
|
||||||
|
"linux-x64": Platform(pypi_suffix="manylinux2014_x86_64",
|
||||||
|
blender_tag="linux-x64"),
|
||||||
|
"macos-arm64": Platform(pypi_suffix="macosx_12_0_arm64",
|
||||||
|
blender_tag="macos-arm64"),
|
||||||
|
"macos-x64": Platform(pypi_suffix="macosx_10_16_x86_64",
|
||||||
|
blender_tag="macos-x64")}
|
||||||
|
|
||||||
|
packages_to_remove = {
|
||||||
|
"imagecodecs",
|
||||||
|
"numpy"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_python(args: str):
|
||||||
|
python = Path(sys.executable).resolve()
|
||||||
|
subprocess.run([python] + args.split(" "))
|
||||||
|
|
||||||
|
|
||||||
|
def build_extension(platform: Platform, python_version="3.11") -> None:
|
||||||
|
wheel_dirpath = Path("./bioxelnodes/wheels")
|
||||||
|
toml_filepath = Path("bioxelnodes/blender_manifest.toml")
|
||||||
|
scipy_ndimage_dirpath = Path("./scipy_ndimage", platform.blender_tag)
|
||||||
|
|
||||||
|
# download required_packages
|
||||||
|
run_python(
|
||||||
|
f"-m pip download {' '.join(required_packages)} --dest {wheel_dirpath.as_posix()} --only-binary=:all: --python-version={python_version} --platform={platform.pypi_suffix}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for f in wheel_dirpath.glob('*.whl'):
|
||||||
|
if any([package in f.name for package in packages_to_remove]):
|
||||||
|
f.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
elif platform.blender_tag == "macos-arm64" and \
|
||||||
|
"lxml" in f.name and "universal2" in f.name:
|
||||||
|
f.rename(Path(f.parent,
|
||||||
|
f.name.replace("universal2", "arm64")))
|
||||||
|
|
||||||
|
for ndimage_filepath in scipy_ndimage_dirpath.iterdir():
|
||||||
|
to_filepath = Path("./bioxelnodes/bioxel/scipy", ndimage_filepath.name)
|
||||||
|
shutil.copy(ndimage_filepath, to_filepath)
|
||||||
|
|
||||||
|
# Load the TOML file
|
||||||
|
with toml_filepath.open("r") as file:
|
||||||
|
manifest = tomlkit.parse(file.read())
|
||||||
|
|
||||||
|
manifest["platforms"] = [platform.blender_tag]
|
||||||
|
manifest["wheels"] = [f"./wheels/{f.name}"
|
||||||
|
for f in wheel_dirpath.glob('*.whl')]
|
||||||
|
|
||||||
|
# build = tomlkit.table(True)
|
||||||
|
# generated = tomlkit.table()
|
||||||
|
# generated["platforms"] = [platform.blender_tag]
|
||||||
|
# generated["wheels"] = [f"./wheels/{f.name}"
|
||||||
|
# for f in wheel_dirpath.glob('*.whl')]
|
||||||
|
|
||||||
|
# build.append('generated', generated)
|
||||||
|
# manifest.append('build', build)
|
||||||
|
|
||||||
|
# Write the updated TOML file
|
||||||
|
with toml_filepath.open("w") as file:
|
||||||
|
text = tomlkit.dumps(manifest)
|
||||||
|
file.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
platform_name = sys.argv[1]
|
||||||
|
platform = platforms[platform_name]
|
||||||
|
build_extension(platform)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -13,7 +13,7 @@ Bioxel Nodes is a Blender addon for scientific volumetric data visualization. It
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Fantastic rendering result, also support EEVEE NEXT.
|
- Realistic rendering result, also support EEVEE NEXT.
|
||||||
- Support multiple formats.
|
- Support multiple formats.
|
||||||
- Support 4D volumetric data.
|
- Support 4D volumetric data.
|
||||||
- All kinds of cutters.
|
- All kinds of cutters.
|
||||||
@@ -41,13 +41,13 @@ Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any
|
|||||||
|
|
||||||
## Support 4D volumetric data
|
## Support 4D volumetric data
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
🥰 4D volumetric data can also be imported into Blender.
|
🥰 4D volumetric data can also be imported into Blender.
|
||||||
|
|
||||||
## Support EEVEE NEXT
|
## Support EEVEE NEXT
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
👍 EEVEE NEXT is absolutely AWESOME! Bioxel Nodes is fully support EEVEE NEXT now! However, there are some limitations:
|
👍 EEVEE NEXT is absolutely AWESOME! Bioxel Nodes is fully support EEVEE NEXT now! However, there are some limitations:
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ Welcome to our [discord server](https://discord.gg/pYkNyq2TjE), if you have any
|
|||||||
|
|
||||||
## Compatible to Newer Version
|
## Compatible to Newer Version
|
||||||
|
|
||||||
**Updating this addon may break old files, so read the following carefully before updating**
|
**v0.3.x is not compatible to v0.2.x, Updating this addon may break old files. Read the following carefully before upgradation**
|
||||||
|
|
||||||
Before updating this addon, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
Before upgradation, you need to ask yourself whether this project file will be modified again or not, if it's an archived project file, I would recommend that you run **Bioxel Nodes > Save Staged Data** to make the addon nodes permanent. In this way, there will be no potential problem with the nodes not functioning due to the addon update.
|
||||||
|
|
||||||
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
After the addon update, your old project files may not work either, this may be because you had executed **Save Staged Data**. If so, you need to execute **Bioxel Nodes > Relink Nodes to Addon** to relink them to make sure that the addon's new functionality and the addon nodes are synchronized.
|
||||||
|
|
||||||
Also, unlike the newer versions, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
Also, the older shaders are not based on OSL, so if you find that you can't render volumes, you need to turn on **Open Shading Language (OSL)** in the Render Settings.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import bpy
|
|
||||||
|
|
||||||
from . import auto_load
|
|
||||||
from . import menus
|
|
||||||
|
|
||||||
|
|
||||||
auto_load.init()
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
|
||||||
auto_load.register()
|
|
||||||
bpy.types.WindowManager.bioxelnodes_progress_factor = bpy.props.FloatProperty()
|
|
||||||
bpy.types.WindowManager.bioxelnodes_progress_text = bpy.props.StringProperty()
|
|
||||||
menus.add()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister():
|
|
||||||
menus.remove()
|
|
||||||
auto_load.unregister()
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
schema_version = "1.0.0"
|
|
||||||
|
|
||||||
# Example of manifest file for a Blender extension
|
|
||||||
# Change the values according to your extension
|
|
||||||
id = "bioxelnodes"
|
|
||||||
version = "0.2.9"
|
|
||||||
name = "Bioxel Nodes"
|
|
||||||
tagline = "For scientific volumetric data visualization in Blender"
|
|
||||||
maintainer = "Ma Nan <icrdr2010@outlook.com>"
|
|
||||||
# Supported types: "add-on", "theme"
|
|
||||||
type = "add-on"
|
|
||||||
|
|
||||||
# Optional link to documentation, support, source files, etc
|
|
||||||
website = "https://omoolab.github.io/BioxelNodes/latest"
|
|
||||||
|
|
||||||
# Optional list defined by Blender and server, see:
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
|
||||||
tags = ["Geometry Nodes", "Render", "Import-Export"]
|
|
||||||
|
|
||||||
blender_version_min = "4.2.0"
|
|
||||||
# Optional: maximum supported Blender version
|
|
||||||
# blender_version_max = "5.1.0"
|
|
||||||
|
|
||||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
|
||||||
license = [
|
|
||||||
"SPDX:MIT",
|
|
||||||
]
|
|
||||||
# Optional: required by some licenses.
|
|
||||||
copyright = [
|
|
||||||
"2024 OmooLab"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
|
|
||||||
platforms = ["windows-x64"]
|
|
||||||
# Other supported platforms: "windows-arm64", "macos-x64"
|
|
||||||
|
|
||||||
# Optional: bundle 3rd party Python modules.
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
|
||||||
wheels = [
|
|
||||||
"./wheels/SimpleITK-2.3.1-cp311-cp311-win_amd64.whl",
|
|
||||||
"./wheels/lxml-5.2.2-cp311-cp311-win_amd64.whl",
|
|
||||||
"./wheels/tifffile-2024.7.21-py3-none-any.whl",
|
|
||||||
"./wheels/pyometiff-1.0.0-py3-none-any.whl",
|
|
||||||
"./wheels/mrcfile-1.5.1-py2.py3-none-any.whl"
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
## Optional: add-ons can list which resources they will require:
|
|
||||||
## * files (for access of any filesystem operations)
|
|
||||||
## * network (for internet access)
|
|
||||||
## * clipboard (to read and/or write the system clipboard)
|
|
||||||
## * camera (to capture photos and videos)
|
|
||||||
## * microphone (to capture audio)
|
|
||||||
##
|
|
||||||
## If using network, remember to also check `bpy.app.online_access`
|
|
||||||
## https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
|
||||||
##
|
|
||||||
## For each permission it is important to also specify the reason why it is required.
|
|
||||||
## Keep this a single short sentence without a period (.) at the end.
|
|
||||||
## For longer explanations use the documentation or detail page.
|
|
||||||
#
|
|
||||||
[permissions]
|
|
||||||
files = "Import/export volume data from/to disk"
|
|
||||||
|
|
||||||
# Optional: build settings.
|
|
||||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
|
||||||
# [build]
|
|
||||||
# paths_exclude_pattern = [
|
|
||||||
# "__pycache__/",
|
|
||||||
# "/.git/",
|
|
||||||
# "/*.zip",
|
|
||||||
# ]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import bpy
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
class BioxelNodesPreferences(bpy.types.AddonPreferences):
|
|
||||||
bl_idname = __package__
|
|
||||||
|
|
||||||
cache_dir: bpy.props.StringProperty(
|
|
||||||
name="Set Cache Directory",
|
|
||||||
subtype='DIR_PATH',
|
|
||||||
default=str(Path(Path.home(), '.bioxelnodes'))
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
do_change_render_setting: bpy.props.BoolProperty(
|
|
||||||
name="Change Render Setting",
|
|
||||||
default=True,
|
|
||||||
) # type: ignore
|
|
||||||
|
|
||||||
def draw(self, context):
|
|
||||||
layout = self.layout
|
|
||||||
|
|
||||||
layout.label(text="Configuration")
|
|
||||||
layout.prop(self, 'cache_dir')
|
|
||||||
layout.prop(self, "do_change_render_setting")
|
|
||||||
1631
poetry.lock
generated
1631
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,29 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bioxelnodes"
|
name = "bioxelnodes"
|
||||||
version = "0.2.9"
|
version = "1.0.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
authors = ["Ma Nan <icrdr2010@outlook.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "~=3.10.0"
|
python = ">=3.11.0,<3.12.dev0"
|
||||||
bpy = "~=4.0"
|
simpleitk = "2.3.1"
|
||||||
simpleitk = "^2.3.1"
|
pyometiff = "1.0.0"
|
||||||
scikit-image = "^0.24.0"
|
mrcfile = "1.5.1"
|
||||||
pyometiff = "^1.0.0"
|
h5py = "3.11.0"
|
||||||
mrcfile = "^1.5.1"
|
transforms3d = "0.4.2"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ipykernel = "^6.25.2"
|
bpy = "^4.2.0"
|
||||||
pytest = "^7.4.2"
|
pytest = "^8.3.2"
|
||||||
pytest-dependency = "^0.5.1"
|
pytest-dependency = "^0.6.0"
|
||||||
mkdocs = "^1.5.3"
|
pytest-cov = "^5.0.0"
|
||||||
mkdocs-material = "^9.4.6"
|
mkdocs = "^1.6.0"
|
||||||
mkdocstrings = { extras = ["python"], version = "^0.23.0" }
|
mkdocs-material = "^9.5.30"
|
||||||
mkdocs-click = "^0.8.1"
|
autopep8 = "^2.3.1"
|
||||||
autopep8 = "^2.0.4"
|
mike = "^2.1.2"
|
||||||
pytest-cov = "^4.1.0"
|
tomlkit = "^0.13.0"
|
||||||
mike = "^2.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
SimpleITK==2.3.1 # Insight Segmentation and Registration Toolkit.
|
||||||
pyometiff==1.0.0 # .ome.tiff
|
pyometiff==1.0.0 # .ome.tiff
|
||||||
mrcfile==1.5.1 # .map
|
mrcfile==1.5.1 # .map
|
||||||
|
h5py==3.11.0 # .h5
|
||||||
|
transforms3d==0.4.2 # transforms
|
||||||
Binary file not shown.
BIN
scipy_ndimage/macos-arm64/_nd_image.cpython-311-darwin.so
Normal file
BIN
scipy_ndimage/macos-arm64/_nd_image.cpython-311-darwin.so
Normal file
Binary file not shown.
BIN
scipy_ndimage/macos-x64/_nd_image.cpython-311-darwin.so
Normal file
BIN
scipy_ndimage/macos-x64/_nd_image.cpython-311-darwin.so
Normal file
Binary file not shown.
Reference in New Issue
Block a user