Update SnapForm to version 0.10.10 and add technical documentation

- Bump SnapForm version in uv.lock from 0.9.30 to 0.10.10.
- Introduce comprehensive technical documentation for SnapForm, detailing features, usage instructions, and development guidelines.
This commit is contained in:
2026-08-08 19:05:42 +08:00
parent d08d0e9c1a
commit aee460fe7f
22 changed files with 3905 additions and 1472 deletions

View File

@@ -1,3 +1,5 @@
import re
import shutil
from pathlib import Path
import bpy
@@ -22,6 +24,17 @@ IMAGE_NAME_SUFFIXES = (
".bmp",
".tga",
)
VIDEO_NAME_SUFFIXES = (
".mp4",
".mov",
".avi",
".mkv",
".webm",
".m4v",
".mts",
".mpg",
".mpeg",
)
def image_depth_model_items(owner, context):
@@ -47,6 +60,56 @@ def is_image_empty(candidate):
)
def is_animated_image(image):
return getattr(image, "source", None) in {"MOVIE", "SEQUENCE"}
def video_total_frames(image):
"""Return the total animated frame count, or 1 for a still image."""
source = getattr(image, "source", None)
if source == "MOVIE":
return int(getattr(image, "frame_duration", 1) or 1)
if source == "SEQUENCE":
return len(collect_sequence_frames(image)) or 1
return 1
def image_user_settings(source_object):
"""Return the Empty's image user settings as a four-item tuple."""
image_user = getattr(source_object, "image_user", None)
if image_user is None:
return 1, 0, 0, False
return (
int(getattr(image_user, "frame_start", 1) or 1),
int(getattr(image_user, "frame_offset", 0) or 0),
int(getattr(image_user, "frame_duration", 0) or 0),
bool(getattr(image_user, "use_cyclic", False)),
)
def extract_frame_range(source_object):
"""Return the clamped (start_frame, frame_count) range extracted from the source."""
total_frames = video_total_frames(getattr(source_object, "data", None))
if total_frames <= 1:
return 1, total_frames
_frame_start, frame_offset, image_frames, _use_cyclic = (
image_user_settings(source_object)
)
frame_count = (
min(image_frames, total_frames)
if image_frames > 1
else total_frames
)
start_frame = min(1 + frame_offset, total_frames - frame_count + 1)
start_frame = max(start_frame, 1)
return start_frame, frame_count
def default_extract_frames(source_object):
"""Return the Empty's Image Frames value, or the full frame count when unset."""
return extract_frame_range(source_object)[1]
def image_path(image):
filepath_from_user = getattr(image, "filepath_from_user", None)
filepath = (
@@ -65,10 +128,22 @@ def image_is_packed(image):
return bool(getattr(image, "packed_files", ()))
def remove_temporary_input(path):
"""Remove a temporary input file or frame directory."""
path = Path(path)
try:
if path.is_dir():
shutil.rmtree(path, ignore_errors=True)
else:
path.unlink(missing_ok=True)
except OSError:
pass
def image_base_name(image):
name = image.name
lower_name = name.lower()
for suffix in IMAGE_NAME_SUFFIXES:
for suffix in IMAGE_NAME_SUFFIXES + VIDEO_NAME_SUFFIXES:
suffix_index = lower_name.rfind(suffix)
if suffix_index <= 0:
continue
@@ -85,8 +160,15 @@ def depth_data_name(source_object):
return f"{image_base_name(source_object.data)}.depth"
def copy_depth_image(source_object):
"""Return a renamed copy of the source image for use as the depth map."""
depth_image = source_object.data.copy()
depth_image.name = depth_data_name(source_object)
return depth_image
def removed_background_data_name(source_object):
return f"{image_base_name(source_object.data)}.rb"
return f"{image_base_name(source_object.data)}.foreground"
def scaled_process_resolution(image, scale):
@@ -117,10 +199,60 @@ def save_packed_image(image, scene):
return output_path
def collect_sequence_frames(image):
"""Return the numbered frame files of a SEQUENCE image, or [] when absent."""
source_path = image_path(image)
if not is_animated_image(image) or not source_path.is_file():
return []
match = re.match(
r"^(?P<prefix>.*?)(?P<digits>\d+)(?P<suffix>\.[^.]+)$",
source_path.name,
)
if match is None:
return []
prefix = match.group("prefix")
suffix = match.group("suffix").lower()
frames = []
for path in source_path.parent.iterdir():
frame_match = re.match(
r"^(?P<prefix>.*?)(?P<digits>\d+)(?P<suffix>\.[^.]+)$",
path.name,
)
if (
frame_match is not None
and frame_match.group("prefix") == prefix
and frame_match.group("suffix").lower() == suffix
):
frames.append((int(frame_match.group("digits")), path))
return [path for _digits, path in sorted(frames)]
def prepare_image_input(image, scene):
source_path = image_path(image)
if image_is_packed(image):
return save_packed_image(image, scene), True
if getattr(image, "source", None) == "MOVIE":
if not source_path.is_file():
raise RuntimeError(
"The image empty must reference an existing video file"
)
return source_path, False
sequence_frames = collect_sequence_frames(image)
if sequence_frames:
from .runtime import temporary_input_directory
temporary_directory = Path(temporary_input_directory())
try:
for index, frame_path in enumerate(sequence_frames):
suffix = frame_path.suffix.lower() or ".jpg"
shutil.copy2(
frame_path,
temporary_directory / f"frame.{index + 1:04d}{suffix}",
)
except Exception:
shutil.rmtree(temporary_directory, ignore_errors=True)
raise
return temporary_directory, True
if not source_path.is_file():
raise RuntimeError(
"The image empty must reference an existing image file"
@@ -128,12 +260,207 @@ def prepare_image_input(image, scene):
return source_path, False
def configure_image_user(
image_user,
frame_count=None,
frame_start=1,
frame_offset=0,
use_cyclic=None,
):
"""Map the image to scene frames and enable auto refresh."""
if image_user is None:
return
if hasattr(image_user, "frame_start"):
image_user.frame_start = frame_start
if hasattr(image_user, "frame_offset"):
image_user.frame_offset = frame_offset
if hasattr(image_user, "use_auto_refresh"):
image_user.use_auto_refresh = True
if use_cyclic is not None and hasattr(image_user, "use_cyclic"):
image_user.use_cyclic = use_cyclic
if frame_count is not None and hasattr(image_user, "frame_duration"):
image_user.frame_duration = frame_count
def pack_sequence_if_possible(image, expected_frames):
"""Pack a frame sequence when Blender supports it; otherwise keep external files."""
if expected_frames <= 0:
return False
try:
image.pack()
except (OSError, RuntimeError):
return False
if len(getattr(image, "packed_files", ())) >= expected_frames:
return True
try:
image.unpack(method="REMOVE")
except (OSError, RuntimeError):
pass
return False
def sorted_frame_paths(directory, prefix=None):
"""Return image files in a directory sorted by their trailing frame number."""
def frame_key(path):
match = re.search(r"(\d+)(?=\.[^.]+$)", path.name)
return (int(match.group(1)) if match is not None else 0, path.name)
return sorted(
(
path
for path in Path(directory).iterdir()
if path.is_file()
and path.suffix.lower() in IMAGE_NAME_SUFFIXES
and (prefix is None or path.stem.startswith(prefix))
),
key=frame_key,
)
def force_sequence_source(image):
"""Ensure a loaded image is a SEQUENCE datablock for its numbered frames."""
if getattr(image, "source", None) == "SEQUENCE":
return image
try:
image.source = "SEQUENCE"
except (OSError, RuntimeError, TypeError) as error:
raise RuntimeError(
"Blender cannot load the frame sequence"
) from error
if getattr(image, "source", None) != "SEQUENCE":
raise RuntimeError("Blender cannot load the frame sequence")
return image
def create_sequence_image(name, frame_directory, frame_prefix="frame."):
"""Load a numbered frame directory as a SEQUENCE image datablock."""
frame_paths = sorted_frame_paths(frame_directory, frame_prefix)
if not frame_paths:
raise RuntimeError(f"No image frames found in: {frame_directory}")
try:
image = bpy.data.images.load(
str(frame_paths[0]),
check_existing=False,
)
force_sequence_source(image)
except (OSError, RuntimeError) as error:
if "image" in locals():
bpy.data.images.remove(image, do_unlink=True)
raise RuntimeError("Unable to load the frame sequence") from error
image.name = name
pack_sequence_if_possible(image, len(frame_paths))
return image
def create_depth_sequence_image(name, depth_frames, scene, frame_directory):
"""Write one EXR per depth frame and return a SEQUENCE image datablock."""
import numpy as np
frame_directory = Path(frame_directory)
frame_directory.mkdir(parents=True, exist_ok=True)
height, width = depth_frames[0].shape
image = bpy.data.images.new(
name,
width=width,
height=height,
alpha=False,
float_buffer=True,
)
image.colorspace_settings.name = "Non-Color"
image_settings = scene.render.image_settings
original_file_format = image_settings.file_format
frame_paths = []
try:
image_settings.file_format = "OPEN_EXR"
for frame_index, depth_values in enumerate(depth_frames):
# NPZ arrays use a top-left origin, while Blender image pixels start below.
depth_pixels = np.flipud(depth_values)
rgba = np.empty((height, width, 4), dtype=np.float32)
rgba[..., 0] = depth_pixels
rgba[..., 1] = depth_pixels
rgba[..., 2] = depth_pixels
rgba[..., 3] = 1.0
image.pixels.foreach_set(rgba.ravel())
frame_path = (
frame_directory / f"depth.{frame_index + 1:04d}.exr"
)
image.save_render(str(frame_path), scene=scene)
if not frame_path.is_file():
raise RuntimeError(f"Blender did not write: {frame_path}")
frame_paths.append(frame_path)
finally:
image_settings.file_format = original_file_format
bpy.data.images.remove(image, do_unlink=True)
if not frame_paths:
raise RuntimeError("Unable to write the depth frame sequence")
try:
sequence_image = bpy.data.images.load(
str(frame_paths[0]),
check_existing=False,
)
force_sequence_source(sequence_image)
except (OSError, RuntimeError) as error:
if "sequence_image" in locals():
bpy.data.images.remove(sequence_image, do_unlink=True)
raise RuntimeError(
"Unable to load the depth frame sequence"
) from error
sequence_image.name = name
sequence_image.colorspace_settings.name = "Non-Color"
pack_sequence_if_possible(sequence_image, len(frame_paths))
return sequence_image
def create_removed_background_image(
source_object,
output_path,
assign_to_source=True,
):
output_path = Path(output_path)
if output_path.is_dir():
foreground_paths = sorted_frame_paths(output_path, "foreground.")
if not foreground_paths:
raise RuntimeError(
f"Background removal result has no foreground frames: "
f"{output_path}"
)
try:
result_image = bpy.data.images.load(
str(foreground_paths[0]),
check_existing=False,
)
force_sequence_source(result_image)
except (OSError, RuntimeError) as error:
if "result_image" in locals():
bpy.data.images.remove(result_image, do_unlink=True)
raise RuntimeError("Unable to load the background result") from error
source_image = source_object.data
original_fake_user = source_image.use_fake_user
try:
result_image.name = removed_background_data_name(source_object)
pack_sequence_if_possible(result_image, len(foreground_paths))
if assign_to_source:
image_user = getattr(source_object, "image_user", None)
(
original_frame_start,
_original_frame_offset,
_original_frame_duration,
original_use_cyclic,
) = image_user_settings(source_object)
source_image.use_fake_user = True
source_object.data = result_image
configure_image_user(
image_user,
len(foreground_paths),
frame_start=original_frame_start,
use_cyclic=original_use_cyclic,
)
except (OSError, RuntimeError) as error:
source_image.use_fake_user = original_fake_user
bpy.data.images.remove(result_image)
raise RuntimeError("Unable to load the background result") from error
return result_image
if not output_path.is_file():
raise RuntimeError(
f"Background removal result does not exist: {output_path}"
@@ -229,7 +556,7 @@ def apply_plane_transform(plane_object, source_object, camera_bounds):
return abs(float(plane_object.scale.z))
def extract_depth_array(npz_path):
def extract_depth_arrays(npz_path):
try:
import numpy as np
except ImportError as error:
@@ -256,13 +583,17 @@ def extract_depth_array(npz_path):
if depth.ndim >= 3 and depth.shape[-1] == 1:
depth = depth[..., 0]
if depth.ndim == 3:
depth = depth[0]
if depth.ndim != 2:
if depth.ndim == 2:
depth = depth[None, ...]
if depth.ndim != 3:
raise ValueError(
f"Unsupported depth array shape: {tuple(depth.shape)}"
)
return depth
return [depth[index] for index in range(depth.shape[0])]
def extract_depth_array(npz_path):
return extract_depth_arrays(npz_path)[0]
def extract_intrinsics_array(npz_path):
@@ -475,6 +806,13 @@ def create_material(
displacement_midlevel=0.0,
displacement_scale=1.0,
unshaded=False,
frame_count=1,
frame_start=1,
color_frame_offset=0,
color_frame_duration=None,
depth_frame_offset=0,
depth_frame_duration=None,
use_cyclic=None,
):
material = bpy.data.materials.new(name)
material.use_nodes = True
@@ -491,6 +829,25 @@ def create_material(
color_texture.extension = "EXTEND"
color_texture.location = (-640.0, 180.0)
output.location = (460.0, 180.0)
extracted_frames = frame_count if frame_count > 1 else None
color_frames = (
color_frame_duration
if color_frame_duration is not None
else extracted_frames
)
depth_frames = (
depth_frame_duration
if depth_frame_duration is not None
else extracted_frames
)
if frame_count > 1 or is_animated_image(color_image):
configure_image_user(
color_texture.image_user,
color_frames,
frame_start=frame_start,
frame_offset=color_frame_offset,
use_cyclic=use_cyclic,
)
links = material.node_tree.links
if unshaded:
@@ -558,6 +915,14 @@ def create_material(
depth_texture.image = displacement_image
depth_texture.interpolation = "Linear"
depth_texture.extension = "EXTEND"
if frame_count > 1 or is_animated_image(displacement_image):
configure_image_user(
depth_texture.image_user,
depth_frames,
frame_start=frame_start,
frame_offset=depth_frame_offset,
use_cyclic=use_cyclic,
)
displacement.inputs["Midlevel"].default_value = (
displacement_midlevel
)
@@ -612,8 +977,35 @@ def build_image_depth_plane(
displacement_via="MODIFIER",
unshaded=False,
color_image=None,
depth_frames_directory=None,
):
source_name = source_object.name
frame_count = 1
frames_image = None
if depth_frames_directory is not None:
frames_image = create_sequence_image(
depth_data_name(source_object),
depth_frames_directory,
)
frame_count = len(
sorted_frame_paths(depth_frames_directory, "frame.")
)
(
source_frame_start,
source_frame_offset,
source_frame_duration,
source_use_cyclic,
) = image_user_settings(source_object)
uses_extracted = frames_image is not None
texture_frame_count = (
frame_count if uses_extracted else source_frame_duration
)
texture_frame_offset = 0 if uses_extracted else source_frame_offset
depth_source_image = source_object.data
if frames_image is not None:
depth_source_image = frames_image
elif not is_animated_image(source_object.data):
depth_source_image = copy_depth_image(source_object)
mesh = create_plane_mesh(
source_object,
subdivision_level=subdivision_level,
@@ -632,25 +1024,43 @@ def build_image_depth_plane(
depth_data_name(source_object),
type="IMAGE",
)
texture.image = source_object.data
texture.image = depth_source_image
texture.extension = "EXTEND"
if uses_extracted or is_animated_image(source_object.data):
configure_image_user(
texture.image_user,
texture_frame_count,
frame_start=source_frame_start,
frame_offset=texture_frame_offset,
use_cyclic=source_use_cyclic,
)
displace = plane_object.modifiers.new("Displace", "DISPLACE")
displace.texture = texture
displace.texture_coords = "UV"
displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL
depth_scale = abs(float(plane_object.scale.z))
displacement_image = (
depth_source_image if displacement_via == "MATERIAL" else None
)
material = create_material(
source_name,
color_image if color_image is not None else source_object.data,
depth_scale,
displacement_image=(
source_object.data if displacement_via == "MATERIAL" else None
),
displacement_image=displacement_image,
displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL,
displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH,
unshaded=unshaded,
frame_count=frame_count,
frame_start=source_frame_start,
color_frame_offset=texture_frame_offset,
color_frame_duration=texture_frame_count,
depth_frame_offset=texture_frame_offset,
depth_frame_duration=texture_frame_count,
use_cyclic=source_use_cyclic,
)
mesh.materials.append(material)
plane_object["snapform_frame_count"] = frame_count
return finish_depth_plane(
context,
source_object,
@@ -669,9 +1079,11 @@ def build_depth_plane(
unshaded=False,
depth_name="",
color_image=None,
depth_frames_directory=None,
):
source_name = source_object.name
depth = extract_depth_array(npz_path)
depth_arrays = extract_depth_arrays(npz_path)
depth = depth_arrays[0]
intrinsics = extract_intrinsics_array(npz_path)
depth_values = prepare_depth_values(depth)
bounds, reference_depth = camera_plane_bounds(
@@ -681,10 +1093,25 @@ def build_depth_plane(
DISPLACEMENT_STRENGTH,
)
depth_name = depth_name or depth_data_name(source_object)
frame_count = len(depth_arrays)
(
source_frame_start,
source_frame_offset,
source_frame_duration,
source_use_cyclic,
) = image_user_settings(source_object)
depth_image = create_depth_image(
depth_name,
depth_values,
)
if frame_count > 1 and depth_frames_directory is not None:
bpy.data.images.remove(depth_image, do_unlink=True)
depth_image = create_depth_sequence_image(
depth_name,
[prepare_depth_values(frame) for frame in depth_arrays],
context.scene,
depth_frames_directory,
)
mesh = create_plane_mesh(source_object, bounds, subdivision_level)
plane_object = bpy.data.objects.new(
source_name,
@@ -709,6 +1136,13 @@ def build_depth_plane(
)
texture.image = depth_image
texture.extension = "EXTEND"
if frame_count > 1:
configure_image_user(
texture.image_user,
frame_count,
frame_start=source_frame_start,
use_cyclic=source_use_cyclic,
)
displace = plane_object.modifiers.new("Displace", "DISPLACE")
displace.texture = texture
displace.texture_coords = "UV"
@@ -726,12 +1160,22 @@ def build_depth_plane(
displacement_midlevel=DISPLACEMENT_MIDLEVEL,
displacement_scale=DISPLACEMENT_STRENGTH,
unshaded=unshaded,
frame_count=frame_count,
frame_start=source_frame_start,
color_frame_offset=0 if color_image is not None else source_frame_offset,
color_frame_duration=(
frame_count if color_image is not None else source_frame_duration
),
depth_frame_offset=0,
depth_frame_duration=frame_count,
use_cyclic=source_use_cyclic,
)
mesh.materials.append(material)
plane_object["snapform_reference_depth"] = reference_depth
plane_object["snapform_camera_plane_width"] = bounds[1] - bounds[0]
plane_object["snapform_camera_plane_height"] = bounds[3] - bounds[2]
plane_object["snapform_depth_scale"] = depth_scale
plane_object["snapform_frame_count"] = frame_count
return finish_depth_plane(
context,
@@ -758,6 +1202,12 @@ class BuildDepthPlane(bpy.types.Operator):
snapform_background_path: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_color_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_depth_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
@@ -806,6 +1256,11 @@ class BuildDepthPlane(bpy.types.Operator):
self.snapform_background_path,
assign_to_source=False,
)
elif self.snapform_color_frames_dir:
color_image = create_sequence_image(
f"{image_base_name(source_object.data)}.frame",
self.snapform_color_frames_dir,
)
build_depth_plane(
context,
source_object,
@@ -816,6 +1271,9 @@ class BuildDepthPlane(bpy.types.Operator):
unshaded=self.snapform_unshaded,
depth_name=self.snapform_depth_data_name,
color_image=color_image,
depth_frames_directory=(
self.snapform_depth_frames_dir or None
),
)
except (OSError, RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
@@ -834,6 +1292,12 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
snapform_background_path: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_color_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_depth_frames_dir: bpy.props.StringProperty(
options={"HIDDEN", "SKIP_SAVE"},
)
snapform_subdivision_level: bpy.props.IntProperty(
default=5,
min=0,
@@ -866,11 +1330,18 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
)
return {"CANCELLED"}
try:
color_image = create_removed_background_image(
source_object,
self.snapform_background_path,
assign_to_source=False,
)
color_image = None
if self.snapform_background_path:
color_image = create_removed_background_image(
source_object,
self.snapform_background_path,
assign_to_source=False,
)
elif self.snapform_color_frames_dir:
color_image = create_sequence_image(
f"{image_base_name(source_object.data)}.frame",
self.snapform_color_frames_dir,
)
build_image_depth_plane(
context,
source_object,
@@ -879,6 +1350,9 @@ class BuildOriginalDepthPlane(bpy.types.Operator):
displacement_via=self.snapform_displacement_via,
unshaded=self.snapform_unshaded,
color_image=color_image,
depth_frames_directory=(
self.snapform_depth_frames_dir or None
),
)
except (OSError, RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
@@ -938,6 +1412,11 @@ class RemoveImageBackground(bpy.types.Operator):
from .operators import IMAGE_BACKGROUND_JOB
source_object = context.object
total_frames = video_total_frames(source_object.data)
if total_frames > 1:
context.scene.snapform_settings.video_frames = (
default_extract_frames(source_object)
)
try:
source_path, delete_input = prepare_image_input(
source_object.data,
@@ -956,7 +1435,7 @@ class RemoveImageBackground(bpy.types.Operator):
if "CANCELLED" not in result:
return {"FINISHED"}
if delete_input:
source_path.unlink(missing_ok=True)
remove_temporary_input(source_path)
return {"CANCELLED"}
@@ -1089,13 +1568,45 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
layout.prop(self, "snapform_delete_original")
def execute(self, context):
from .operators import IMAGE_DEPTH_JOB, ORIGINAL_BACKGROUND_JOB
from .operators import (
IMAGE_DEPTH_JOB,
ORIGINAL_BACKGROUND_JOB,
ORIGINAL_FRAMES_JOB,
)
source_object = context.object
if video_total_frames(source_object.data) > 1:
context.scene.snapform_settings.video_frames = (
default_extract_frames(source_object)
)
if (
self.snapform_depth_source == "ORIGINAL"
and not self.snapform_remove_background
):
if video_total_frames(source_object.data) > 1:
try:
source_path, delete_input = prepare_image_input(
source_object.data,
context.scene,
)
except RuntimeError as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
result = bpy.ops.snapform.run_job(
job_type=ORIGINAL_FRAMES_JOB,
snapform_input_path=str(source_path),
snapform_source_object_name=source_object.name,
snapform_subdivision_level=self.snapform_subdivision_level,
snapform_delete_source=self.snapform_delete_original,
snapform_delete_input=delete_input,
snapform_displacement_via=self.snapform_displacement_via,
snapform_unshaded=self.snapform_unshaded,
)
if "CANCELLED" not in result:
return {"FINISHED"}
if delete_input:
remove_temporary_input(source_path)
return {"CANCELLED"}
try:
build_image_depth_plane(
context,
@@ -1141,7 +1652,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
if "CANCELLED" not in result:
return {"FINISHED"}
if delete_input:
source_path.unlink(missing_ok=True)
remove_temporary_input(source_path)
return {"CANCELLED"}
image = source_object.data
@@ -1179,7 +1690,7 @@ class ConvertImageToDepthPlane(bpy.types.Operator):
)
if "CANCELLED" in result:
if delete_input:
source_path.unlink(missing_ok=True)
remove_temporary_input(source_path)
return {"CANCELLED"}
global _last_depth_plane_model
_last_depth_plane_model = self.snapform_model