796 lines
26 KiB
Python
796 lines
26 KiB
Python
import json
|
|
import math
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
import numpy as np
|
|
import SimpleITK as sitk
|
|
|
|
# KeyboardInterrupt replaced with built-in KeyboardInterrupt
|
|
from ..props import BIOXEL_Series
|
|
from ..utils import get_layer_obj, wrapped_label
|
|
|
|
from ..bioxel.parse import DICOM_EXTS, SUPPORT_EXTS, get_ext
|
|
|
|
from ..utils import get_cache_dir, progress_update, progress_bar
|
|
from ..layer import get_layer_caches, set_layer_caches
|
|
|
|
|
|
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
|
|
|
|
|
|
def write_worker_json(path: Path, data):
|
|
path.write_text(json.dumps(data), encoding="utf-8")
|
|
|
|
|
|
def read_worker_json(path: Path):
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def remove_progress_bar_safe():
|
|
try:
|
|
bpy.types.STATUSBAR_HT_header.remove(progress_bar)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def start_worker_process(owner, command: str, payload: dict):
|
|
job_dir = Path(tempfile.mkdtemp(prefix="bioxel_import_", dir=str(get_cache_dir())))
|
|
config_path = job_dir / "config.json"
|
|
progress_path = job_dir / "progress.json"
|
|
result_path = job_dir / "result.json"
|
|
cancel_path = job_dir / "cancel"
|
|
log_path = job_dir / "worker.log"
|
|
|
|
config = {
|
|
**payload,
|
|
"command": command,
|
|
"progress_path": str(progress_path),
|
|
"result_path": str(result_path),
|
|
"cancel_path": str(cancel_path),
|
|
}
|
|
write_worker_json(config_path, config)
|
|
write_worker_json(progress_path, {"factor": 0.0, "text": "Starting..."})
|
|
|
|
worker_path = Path(__file__).with_name("io_worker.py")
|
|
cmd = [
|
|
bpy.app.binary_path,
|
|
"--background",
|
|
"--factory-startup",
|
|
"--python",
|
|
str(worker_path),
|
|
"--",
|
|
str(config_path),
|
|
]
|
|
|
|
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
log_file = log_path.open("w", encoding="utf-8")
|
|
try:
|
|
owner.process = subprocess.Popen(
|
|
cmd,
|
|
stdout=log_file,
|
|
stderr=subprocess.STDOUT,
|
|
creationflags=creationflags,
|
|
)
|
|
finally:
|
|
log_file.close()
|
|
|
|
owner.job_dir = job_dir
|
|
owner.progress_path = progress_path
|
|
owner.result_path = result_path
|
|
owner.cancel_path = cancel_path
|
|
owner.log_path = log_path
|
|
|
|
|
|
def cancel_worker_process(owner, context):
|
|
owner.is_cancelled = True
|
|
try:
|
|
owner.cancel_path.write_text("cancel", encoding="utf-8")
|
|
except Exception:
|
|
pass
|
|
progress_update(context, 0.0, "Canceling...")
|
|
|
|
|
|
def update_worker_progress(owner, context):
|
|
progress = read_worker_json(owner.progress_path)
|
|
if progress:
|
|
progress_update(
|
|
context,
|
|
float(progress.get("factor", 0.0)),
|
|
progress.get("text", ""),
|
|
)
|
|
|
|
"""
|
|
ImportData -> ParseVolumetricData -> ImportDataDialog
|
|
start import parse data execute import
|
|
"""
|
|
|
|
|
|
class ImportDataBase:
|
|
bl_options = {"UNDO"}
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
read_as = None
|
|
|
|
def draw(self, context):
|
|
"""
|
|
Display helpful information in the file browser's properties region
|
|
when the file selector is open for this operator.
|
|
"""
|
|
layout = self.layout
|
|
exts_list = sorted(SUPPORT_EXTS)
|
|
layout.label(text="Supported formats:")
|
|
chunk_size = 6 # adjust per-line count to taste
|
|
for i in range(0, len(exts_list), chunk_size):
|
|
layout.label(text=" ".join(exts_list[i : i + chunk_size]))
|
|
|
|
layout.separator()
|
|
layout.label(text="Tips:")
|
|
wrapped_label(
|
|
layout, "- Use 'Import Data' button in Bioxel panel to import data"
|
|
)
|
|
wrapped_label(
|
|
layout, "- Use 'Import as ...' to choose data type (Scalar/Label/Color)"
|
|
)
|
|
wrapped_label(
|
|
layout, "- If the file contains multiple series, select series after open"
|
|
)
|
|
|
|
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"}
|
|
|
|
if self.read_as is None:
|
|
bpy.ops.bioxel.parse_volumetric_data(
|
|
"INVOKE_DEFAULT", filepath=self.filepath, skip_read_as=False
|
|
)
|
|
else:
|
|
|
|
bpy.ops.bioxel.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 ImportData(bpy.types.Operator, ImportDataBase):
|
|
bl_idname = "bioxel.import_data"
|
|
bl_label = "Import Data"
|
|
bl_description = "Import Volumetric Data to Layer"
|
|
bl_icon = "EVENT_S"
|
|
|
|
|
|
class ImportAsScalar(bpy.types.Operator, ImportDataBase):
|
|
bl_idname = "bioxel.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, ImportDataBase):
|
|
bl_idname = "bioxel.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, ImportDataBase):
|
|
bl_idname = "bioxel.import_as_color"
|
|
bl_label = "Import as Color"
|
|
bl_description = "Import Volumetric Data to Container as Color"
|
|
bl_icon = "EVENT_C"
|
|
read_as = "COLOR"
|
|
|
|
|
|
class BIOXELNODES_FH_ImportData(bpy.types.FileHandler):
|
|
bl_idname = "BIOXELNODES_FH_ImportData"
|
|
bl_label = "File handler for dicom import"
|
|
bl_import_operator = "bioxel.parse_volumetric_data"
|
|
bl_file_extensions = ";".join(SUPPORT_EXTS)
|
|
|
|
@classmethod
|
|
def poll_drop(cls, context):
|
|
return (
|
|
bpy.context.area.type == "NODE_EDITOR"
|
|
and bpy.context.space_data.node_tree.bl_idname == "GeometryNodeTree"
|
|
)
|
|
|
|
|
|
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 = "bioxel.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
|
|
process = None
|
|
job_dir = None
|
|
progress_path = None
|
|
result_path = None
|
|
cancel_path = None
|
|
log_path = 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", "Label", ""),
|
|
("COLOR", "Color", ""),
|
|
],
|
|
) # type: ignore
|
|
|
|
series_id: bpy.props.EnumProperty(
|
|
name="Select Series", items=get_series_ids
|
|
) # type: ignore
|
|
|
|
series_ids: bpy.props.CollectionProperty(type=BIOXEL_Series) # type: ignore
|
|
|
|
def execute(self, context):
|
|
print("Collecting Meta Data...")
|
|
self.is_cancelled = False
|
|
self.has_error = None
|
|
self.meta = None
|
|
self.label_count = 0
|
|
self.dtype = None
|
|
|
|
start_worker_process(
|
|
self,
|
|
"read_meta",
|
|
{
|
|
"filepath": self.filepath,
|
|
"series_id": self.series_id,
|
|
},
|
|
)
|
|
|
|
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":
|
|
cancel_worker_process(self, context)
|
|
return {"PASS_THROUGH"}
|
|
|
|
if event.type != "TIMER":
|
|
return {"PASS_THROUGH"}
|
|
|
|
bpy.context.workspace.status_text_set_internal(None)
|
|
update_worker_progress(self, context)
|
|
|
|
if self.process.poll() is None:
|
|
return {"PASS_THROUGH"}
|
|
|
|
context.window_manager.event_timer_remove(self._timer)
|
|
remove_progress_bar_safe()
|
|
progress_update(context, 1.0)
|
|
|
|
result = read_worker_json(self.result_path)
|
|
if self.is_cancelled or (result and result.get("cancelled")):
|
|
self.report({"WARNING"}, "Canncelled by user.")
|
|
return {"CANCELLED"}
|
|
|
|
if not result:
|
|
self.report({"ERROR"}, f"Import worker failed. See log: {self.log_path}")
|
|
return {"CANCELLED"}
|
|
|
|
if not result.get("ok"):
|
|
print(result.get("traceback", ""))
|
|
self.report({"ERROR"}, result.get("error", "Import worker failed."))
|
|
return {"CANCELLED"}
|
|
|
|
self.meta = result["meta"]
|
|
self.label_count = int(result["label_count"])
|
|
self.dtype = np.dtype(result["dtype"])
|
|
|
|
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"}
|
|
|
|
if self.label_count == 0:
|
|
self.report({"ERROR"}, "Get no label.")
|
|
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)))
|
|
|
|
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
|
|
|
|
series_id = self.series_id if self.series_id != "empty" else ""
|
|
bpy.ops.bioxel.import_volumetric_data_dialog(
|
|
"INVOKE_DEFAULT",
|
|
filepath=self.filepath,
|
|
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"],
|
|
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.bioxel_progress_factor < 1:
|
|
print("A process is executing, please wait for it to finish.")
|
|
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"}
|
|
|
|
title = f"Add to **{context.object.name}**"
|
|
|
|
# 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(" ")
|
|
stirng.encode("utf-8")
|
|
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"
|
|
|
|
label = "{:<20} {:>1}".format(
|
|
f"{study_description}>{series_description}({series_modality})",
|
|
f"({size_x}x{size_y})x{count}",
|
|
)
|
|
|
|
series_items[series_id] = label
|
|
|
|
for series_id, label in series_items.items():
|
|
series_item = self.series_ids.add()
|
|
series_item.id = series_id
|
|
series_item.label = label
|
|
|
|
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 ImportDataDialog(bpy.types.Operator):
|
|
bl_idname = "bioxel.import_volumetric_data_dialog"
|
|
bl_label = "Import Volumetric Data"
|
|
bl_description = "Import Volumetric Data as Layer"
|
|
bl_options = {"UNDO"}
|
|
|
|
cache_infos = None
|
|
added_ids = None
|
|
process = None
|
|
job_dir = None
|
|
progress_path = None
|
|
result_path = None
|
|
cancel_path = None
|
|
log_path = None
|
|
_timer = None
|
|
|
|
filepath: bpy.props.StringProperty(subtype="FILE_PATH") # type: ignore
|
|
layer_name: bpy.props.StringProperty(name="Layer Name") # type: ignore
|
|
series_id: 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
|
|
smooth: bpy.props.IntProperty(
|
|
name="Smooth Size (Larger takes longer time)", default=0
|
|
) # type: ignore
|
|
read_as: bpy.props.EnumProperty(
|
|
name="Read as",
|
|
default="SCALAR",
|
|
items=[
|
|
("SCALAR", "Scalar", ""),
|
|
("LABEL", "Label", ""),
|
|
("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):
|
|
self.is_cancelled = False
|
|
self.has_error = None
|
|
self.cache_infos = None
|
|
self.added_ids = None
|
|
|
|
start_worker_process(
|
|
self,
|
|
"import_layers",
|
|
{
|
|
"filepath": self.filepath,
|
|
"series_id": self.series_id,
|
|
"cache_dir": str(get_cache_dir() / "layers"),
|
|
"layer_name": self.layer_name,
|
|
"orig_shape": list(self.orig_shape),
|
|
"orig_spacing": list(self.orig_spacing),
|
|
"bioxel_size": self.bioxel_size,
|
|
"read_as": self.read_as,
|
|
"frame_source": self.frame_source,
|
|
"smooth": self.smooth,
|
|
"remap": self.remap,
|
|
"split_channel": self.split_channel,
|
|
"channel_count": self.channel_count,
|
|
},
|
|
)
|
|
|
|
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":
|
|
cancel_worker_process(self, context)
|
|
return {"PASS_THROUGH"}
|
|
|
|
if event.type != "TIMER":
|
|
return {"PASS_THROUGH"}
|
|
|
|
bpy.context.workspace.status_text_set_internal(None)
|
|
update_worker_progress(self, context)
|
|
|
|
if self.process.poll() is None:
|
|
return {"PASS_THROUGH"}
|
|
|
|
context.window_manager.event_timer_remove(self._timer)
|
|
remove_progress_bar_safe()
|
|
progress_update(context, 1.0)
|
|
|
|
result = read_worker_json(self.result_path)
|
|
if self.is_cancelled or (result and result.get("cancelled")):
|
|
self.report({"WARNING"}, "Canncelled by user.")
|
|
return {"CANCELLED"}
|
|
|
|
if not result:
|
|
self.report({"ERROR"}, f"Import worker failed. See log: {self.log_path}")
|
|
return {"CANCELLED"}
|
|
|
|
if not result.get("ok"):
|
|
print(result.get("traceback", ""))
|
|
self.report({"ERROR"}, result.get("error", "Import worker failed."))
|
|
return {"CANCELLED"}
|
|
|
|
self.cache_infos = result.get("cache_infos")
|
|
self.added_ids = result.get("added_ids")
|
|
if not self.cache_infos or not self.added_ids:
|
|
self.report({"ERROR"}, "Some thing went wrong.")
|
|
return {"CANCELLED"}
|
|
|
|
is_first_import = len(get_layer_caches()) == 0
|
|
existing_data = get_layer_caches()
|
|
existing_data.extend(self.cache_infos)
|
|
set_layer_caches(existing_data)
|
|
|
|
setattr(context.window_manager, "bioxel_layer_library", self.added_ids[-1])
|
|
|
|
if is_first_import:
|
|
bpy.ops.bioxel.render_setting_preset("EXEC_DEFAULT", preset="balance")
|
|
|
|
self.report({"INFO"}, "Successfully Imported")
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
layer_kind = self.read_as.capitalize()
|
|
title = f"Add to **{context.object.name}**, 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!**"
|
|
|
|
layout = self.layout
|
|
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")
|
|
elif self.read_as == "LABEL":
|
|
panel.prop(self, "smooth")
|
|
|
|
panel.label(text=f"Shape from {orig_shape_text} to {layer_shape_text}")
|
|
panel.label(text="Dimension Order: [Frame, X-axis, Y-axis, Z-axis, Channel]")
|
|
|
|
|
|
class ExportVolumetricData(bpy.types.Operator):
|
|
bl_idname = "bioxel.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"}
|