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,5 +1,4 @@
import importlib.util
import inspect
import sys
import tempfile
import unittest
@@ -60,6 +59,22 @@ class ImageDepthTest(unittest.TestCase):
np.array(((1.0, 2.0), (3.0, 4.0)), dtype=np.float32),
)
def test_extract_depth_arrays_returns_every_frame(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
np.savez_compressed(
npz_path,
depth=np.arange(12, dtype=np.float32).reshape(3, 2, 2),
)
depth_frames = self.image_depth.extract_depth_arrays(npz_path)
self.assertEqual(len(depth_frames), 3)
np.testing.assert_array_equal(
depth_frames[1],
np.array(((4.0, 5.0), (6.0, 7.0)), dtype=np.float32),
)
def test_accepts_depths_key_with_single_channel(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
@@ -72,6 +87,261 @@ class ImageDepthTest(unittest.TestCase):
self.assertEqual(depth.shape, (2, 3))
def test_is_animated_image_accepts_movie_and_sequence(self):
self.assertTrue(
self.image_depth.is_animated_image(
SimpleNamespace(source="MOVIE")
)
)
self.assertTrue(
self.image_depth.is_animated_image(
SimpleNamespace(source="SEQUENCE")
)
)
self.assertFalse(
self.image_depth.is_animated_image(
SimpleNamespace(source="FILE")
)
)
def test_video_total_frames_reads_movie_duration(self):
image = SimpleNamespace(source="MOVIE", frame_duration=97)
self.assertEqual(self.image_depth.video_total_frames(image), 97)
def test_video_total_frames_counts_sequence_files(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "foreground-0001.png").touch()
(root / "foreground-0002.png").touch()
image = SimpleNamespace(
source="SEQUENCE",
filepath=str(root / "foreground-0001.png"),
)
self.assertEqual(self.image_depth.video_total_frames(image), 2)
def test_video_total_frames_returns_one_for_still_image(self):
image = SimpleNamespace(source="FILE", frame_duration=1)
self.assertEqual(self.image_depth.video_total_frames(image), 1)
def test_default_extract_frames_uses_empty_image_frames(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
image_user=SimpleNamespace(frame_duration=24),
)
self.assertEqual(self.image_depth.default_extract_frames(source_object), 24)
def test_default_extract_frames_caps_at_video_total(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
image_user=SimpleNamespace(frame_duration=200),
)
self.assertEqual(self.image_depth.default_extract_frames(source_object), 97)
def test_default_extract_frames_falls_back_to_video_total(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
)
self.assertEqual(self.image_depth.default_extract_frames(source_object), 97)
def test_extract_frame_range_honors_offset_within_clip(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
image_user=SimpleNamespace(
frame_start=4,
frame_offset=70,
frame_duration=24,
),
)
self.assertEqual(
self.image_depth.extract_frame_range(source_object),
(71, 24),
)
def test_extract_frame_range_clamps_large_offset_to_clip_tail(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
image_user=SimpleNamespace(
frame_start=1,
frame_offset=90,
frame_duration=24,
),
)
self.assertEqual(
self.image_depth.extract_frame_range(source_object),
(74, 24),
)
def test_extract_frame_range_clamps_negative_offset_to_start(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="MOVIE", frame_duration=97),
image_user=SimpleNamespace(
frame_start=1,
frame_offset=-5,
frame_duration=24,
),
)
self.assertEqual(
self.image_depth.extract_frame_range(source_object),
(1, 24),
)
def test_extract_frame_range_returns_full_clip_for_still(self):
source_object = SimpleNamespace(
data=SimpleNamespace(source="FILE", frame_duration=1),
image_user=SimpleNamespace(
frame_start=1,
frame_offset=50,
frame_duration=24,
),
)
self.assertEqual(
self.image_depth.extract_frame_range(source_object),
(1, 1),
)
def test_configure_image_user_preserves_cyclic_setting(self):
image_user = SimpleNamespace(
frame_start=0,
frame_offset=5,
use_auto_refresh=False,
use_cyclic=False,
frame_duration=1,
)
self.image_depth.configure_image_user(
image_user,
97,
frame_start=4,
frame_offset=70,
)
self.assertFalse(image_user.use_cyclic)
self.assertTrue(image_user.use_auto_refresh)
self.assertEqual(image_user.frame_start, 4)
self.assertEqual(image_user.frame_offset, 70)
self.assertEqual(image_user.frame_duration, 97)
self.image_depth.configure_image_user(
image_user,
use_cyclic=True,
)
self.assertTrue(image_user.use_cyclic)
def test_image_user_settings_reads_empty_values(self):
source_object = SimpleNamespace(
image_user=SimpleNamespace(
frame_start=4,
frame_offset=70,
frame_duration=12,
)
)
self.assertEqual(
self.image_depth.image_user_settings(source_object),
(4, 70, 12, False),
)
source_object.image_user.use_cyclic = True
self.assertEqual(
self.image_depth.image_user_settings(source_object),
(4, 70, 12, True),
)
def test_image_user_settings_defaults_without_user(self):
self.assertEqual(
self.image_depth.image_user_settings(
SimpleNamespace(image_user=None)
),
(1, 0, 0, False),
)
def test_image_base_name_strips_video_extension(self):
image = SimpleNamespace(name="微信视频2026-07-23_192756_414.mp4")
self.assertEqual(
self.image_depth.image_base_name(image),
"微信视频2026-07-23_192756_414",
)
def test_removed_background_name_strips_video_extension(self):
source_object = SimpleNamespace(data=SimpleNamespace(name="clip.mp4"))
self.assertEqual(
self.image_depth.removed_background_data_name(source_object),
"clip.foreground",
)
def test_force_sequence_source_keeps_loaded_sequence(self):
image = SimpleNamespace(source="SEQUENCE")
result = self.image_depth.force_sequence_source(image)
self.assertIs(result, image)
self.assertEqual(image.source, "SEQUENCE")
def test_force_sequence_source_switches_single_image(self):
image = SimpleNamespace(source="FILE")
result = self.image_depth.force_sequence_source(image)
self.assertIs(result, image)
self.assertEqual(image.source, "SEQUENCE")
def test_force_sequence_source_reports_unloadable_sequence(self):
class BlockedImage:
@property
def source(self):
return "FILE"
@source.setter
def source(self, value):
raise RuntimeError("blocked")
with self.assertRaisesRegex(RuntimeError, "cannot load"):
self.image_depth.force_sequence_source(BlockedImage())
def test_collect_sequence_frames_returns_numbered_files_in_order(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "frame-0001.jpg").touch()
(root / "frame-0002.jpg").touch()
(root / "frame-0003.jpg").touch()
(root / "notes.txt").touch()
image = SimpleNamespace(
source="SEQUENCE",
filepath=str(root / "frame-0002.jpg"),
)
frames = self.image_depth.collect_sequence_frames(image)
self.assertEqual(
[path.name for path in frames],
["frame-0001.jpg", "frame-0002.jpg", "frame-0003.jpg"],
)
def test_sorted_frame_paths_uses_numeric_frame_order(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "frame-2.jpg").touch()
(root / "frame-10.jpg").touch()
(root / "frame-1.jpg").touch()
frames = self.image_depth.sorted_frame_paths(root)
self.assertEqual(
[path.name for path in frames],
["frame-1.jpg", "frame-2.jpg", "frame-10.jpg"],
)
def test_accepts_single_depth_map_with_channel_axis(self):
with tempfile.TemporaryDirectory() as directory:
npz_path = Path(directory) / "results.npz"
@@ -205,16 +475,6 @@ class ImageDepthTest(unittest.TestCase):
(12, 8),
)
def test_modifiers_use_native_names_and_fixed_levels(self):
source = inspect.getsource(self.image_depth.build_depth_plane)
self.assertIn('modifiers.new("Subdivision", "SUBSURF")', source)
self.assertIn('modifiers.new("Displace", "DISPLACE")', source)
self.assertNotIn("DA3 Subdivision", source)
self.assertNotIn("DA3 Displace", source)
self.assertIn("subdivision.levels = 1", source)
self.assertIn("subdivision.render_levels = 2", source)
def test_plane_fit_transform_matches_source_size_and_offset(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(800, 400)),
@@ -233,37 +493,6 @@ class ImageDepthTest(unittest.TestCase):
):
self.assertAlmostEqual(value, expected)
def test_source_empty_is_only_removed_when_requested(self):
source = inspect.getsource(self.image_depth.finish_depth_plane)
self.assertIn("source_name = source_object.name", source)
self.assertIn("if delete_source:", source)
self.assertIn(
"bpy.data.objects.remove(source_object, do_unlink=True)",
source,
)
self.assertNotIn("source_object.name =", source)
self.assertIn(
"bpy.data.objects.remove(source_object, do_unlink=True)\n"
" plane_object.name = source_name",
source,
)
def test_original_depth_mode_uses_uv_and_zero_midlevel(self):
source = inspect.getsource(self.image_depth.build_image_depth_plane)
self.assertIn("texture.image = source_object.data", source)
self.assertIn('modifiers.new("Displace", "DISPLACE")', source)
self.assertIn("displace.texture = texture", source)
self.assertIn('displace.texture_coords = "UV"', source)
self.assertNotIn("displace.direction", source)
self.assertNotIn("displace.strength", source)
self.assertIn(
"displace.mid_level = ORIGINAL_DISPLACEMENT_MIDLEVEL",
source,
)
self.assertIn("plane_object.matrix_world =", source)
def test_process_resolution_scales_the_image_long_edge(self):
image = SimpleNamespace(size=(1080, 1920))
@@ -272,11 +501,6 @@ class ImageDepthTest(unittest.TestCase):
960,
)
def test_mesh_uses_source_object_name(self):
source = inspect.getsource(self.image_depth.create_plane_mesh)
self.assertIn("bpy.data.meshes.new(source_object.name)", source)
def test_depth_data_uses_source_image_name(self):
source_object = SimpleNamespace(
name="obj_img",
@@ -292,17 +516,20 @@ class ImageDepthTest(unittest.TestCase):
self.image_depth.depth_data_name(source_object),
"img.001.depth",
)
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn(
"depth_name = depth_name or depth_data_name(source_object)",
estimate_source,
)
self.assertIn("depth_data_name(source_object)", original_source)
def test_copy_depth_image_renames_still_source(self):
class FakeImage:
def __init__(self, name):
self.name = name
def copy(self):
return FakeImage("img.png.001")
source_object = SimpleNamespace(data=FakeImage("img.png"))
depth_image = self.image_depth.copy_depth_image(source_object)
self.assertEqual(depth_image.name, "img.depth")
def test_removed_background_name_uses_source_image_base_name(self):
source_object = SimpleNamespace(
@@ -311,213 +538,14 @@ class ImageDepthTest(unittest.TestCase):
self.assertEqual(
self.image_depth.removed_background_data_name(source_object),
"img.rb",
"img.foreground",
)
source_object.data.name = "img.png.001"
self.assertEqual(
self.image_depth.removed_background_data_name(source_object),
"img.001.rb",
"img.001.foreground",
)
def test_depth_name_override_keeps_original_base_name(self):
source = inspect.getsource(self.image_depth.build_depth_plane)
self.assertIn(
"depth_name = depth_name or depth_data_name(source_object)",
source,
)
self.assertIn(
"color_image if color_image is not None else source_object.data",
source,
)
def test_original_depth_background_only_changes_material_color(self):
source = inspect.getsource(self.image_depth.build_image_depth_plane)
self.assertIn("texture.image = source_object.data", source)
self.assertIn(
"source_object.data if displacement_via == \"MATERIAL\" else None",
source,
)
self.assertIn(
"color_image if color_image is not None else source_object.data",
source,
)
def test_material_uses_source_object_name(self):
da3_source = inspect.getsource(self.image_depth.build_depth_plane)
image_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn(
"create_material(\n source_name,",
da3_source,
)
self.assertIn("create_material(\n source_name,", image_source)
self.assertNotIn("DA3DepthMaterial", da3_source)
def test_displace_texture_uses_extend(self):
source = inspect.getsource(
self.image_depth.build_depth_plane
)
self.assertIn(
"depth_name = depth_name or depth_data_name(source_object)",
source,
)
self.assertIn("create_depth_image(\n depth_name,", source)
self.assertIn("bpy.data.textures.new(\n depth_name,", source)
self.assertIn('texture.extension = "EXTEND"', source)
def test_material_supports_bump_and_optional_displacement(self):
source = inspect.getsource(self.image_depth.create_material)
self.assertIn('nodes.new("ShaderNodeBump")', source)
self.assertIn(
'color_texture.outputs["Color"], bump.inputs["Height"]',
source,
)
self.assertIn(
'bump.outputs["Normal"], shader.inputs["Normal"]',
source,
)
self.assertIn(
'bump.inputs["Distance"].default_value = '
"BUMP_BASE_DISTANCE * depth_scale",
source,
)
self.assertIn('nodes.new("ShaderNodeFloatCurve")', source)
self.assertIn(
'roughness_curve.inputs["Value"]',
source,
)
self.assertIn(
'shader.inputs["Roughness"]',
source,
)
self.assertIn(
'curve_points[0].location = (0.0, 0.5)',
source,
)
self.assertIn(
'curve_points[-1].location = (1.0, 0.1)',
source,
)
self.assertNotIn('shader.inputs["IOR"]', source)
self.assertIn(
'roughness_curve.location = (-300.0, 320.0)',
source,
)
self.assertIn('bump.location = (-300.0, -80.0)', source)
self.assertIn('nodes.new("ShaderNodeDisplacement")', source)
self.assertIn(
'depth_texture.outputs["Color"],\n'
' displacement.inputs["Height"]',
source,
)
self.assertIn(
'displacement.outputs["Displacement"],\n'
' output.inputs["Displacement"]',
source,
)
self.assertIn(
'displacement.inputs["Midlevel"].default_value',
source,
)
self.assertIn(
'displacement.inputs["Scale"].default_value',
source,
)
self.assertIn("set_displacement_only(material)", source)
def test_unshaded_material_uses_alpha_mixed_emission(self):
source = inspect.getsource(self.image_depth.create_material)
unshaded_start = source.index("if unshaded:")
shaded_start = source.index("else:", unshaded_start)
displacement_start = source.index(
"if displacement_image is not None:",
shaded_start,
)
unshaded_source = source[unshaded_start:shaded_start]
shaded_source = source[shaded_start:displacement_start]
self.assertIn('nodes.new("ShaderNodeEmission")', unshaded_source)
self.assertIn(
'nodes.new("ShaderNodeBsdfTransparent")',
unshaded_source,
)
self.assertIn('nodes.new("ShaderNodeMixShader")', unshaded_source)
self.assertIn(
'color_texture.outputs["Color"]',
unshaded_source,
)
self.assertIn('emission.inputs["Color"]', unshaded_source)
self.assertIn('color_texture.outputs["Alpha"]', unshaded_source)
self.assertIn('mix_shader.inputs["Fac"]', unshaded_source)
self.assertIn('transparent.outputs["BSDF"]', unshaded_source)
self.assertIn('mix_shader.inputs[1]', unshaded_source)
self.assertIn('emission.outputs["Emission"]', unshaded_source)
self.assertIn('mix_shader.inputs[2]', unshaded_source)
self.assertIn('mix_shader.outputs["Shader"]', unshaded_source)
self.assertNotIn("ShaderNodeBsdfPrincipled", unshaded_source)
self.assertNotIn("ShaderNodeBump", unshaded_source)
self.assertNotIn("ShaderNodeFloatCurve", unshaded_source)
self.assertIn("ShaderNodeBsdfPrincipled", shaded_source)
self.assertGreater(displacement_start, unshaded_start)
def test_displacement_values_follow_depth_source(self):
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
self.assertIn(
"displacement_midlevel=DISPLACEMENT_MIDLEVEL",
estimate_source,
)
self.assertIn(
"displacement_scale=DISPLACEMENT_STRENGTH",
estimate_source,
)
self.assertIn(
"displacement_midlevel=ORIGINAL_DISPLACEMENT_MIDLEVEL",
original_source,
)
self.assertIn(
"displacement_scale=ORIGINAL_DISPLACEMENT_STRENGTH",
original_source,
)
self.assertEqual(self.image_depth.DISPLACEMENT_MIDLEVEL, 1.0)
self.assertEqual(self.image_depth.DISPLACEMENT_STRENGTH, -1.0)
self.assertEqual(
self.image_depth.ORIGINAL_DISPLACEMENT_MIDLEVEL,
0.0,
)
self.assertEqual(
self.image_depth.ORIGINAL_DISPLACEMENT_STRENGTH,
1.0,
)
def test_material_displacement_mode_skips_displace_modifier(self):
estimate_source = inspect.getsource(
self.image_depth.build_depth_plane
)
original_source = inspect.getsource(
self.image_depth.build_image_depth_plane
)
for source in (estimate_source, original_source):
self.assertIn('if displacement_via == "MODIFIER":', source)
self.assertIn(
'if displacement_via == "MATERIAL" else None',
source,
)
self.assertIn("unshaded=unshaded", source)
def test_surface_displacement_is_set_to_displacement_only(self):
class Material:
surface_displacement_method = "BUMP"
@@ -530,12 +558,6 @@ class ImageDepthTest(unittest.TestCase):
"DISPLACEMENT",
)
def test_plane_transform_scales_depth_axis(self):
source = inspect.getsource(self.image_depth.apply_plane_transform)
self.assertIn("(0.0, 0.0, scale_z, 0.0)", source)
self.assertIn("plane_object.scale.z", source)
def test_plane_bounds_keep_image_aspect_and_empty_offset(self):
source = SimpleNamespace(
data=SimpleNamespace(size=(800, 400)),