Refactor and enhance SnapForm functionality
- Introduced MODEL_NUMBERS for better model indexing. - Improved enum_items function to cache results and handle download status. - Updated ActiveJob class to manage temporary input paths and added displacement_via property. - Changed IMAGE_DEPTH_PROCESS_RES_METHOD from "lower_bound_resize" to "upper_bound_resize". - Enhanced RunJob operator to include new properties and improved error messages. - Modified panel layout for better user experience and added runtime installation checks. - Updated SnapFormSettings to reflect new model properties and defaults. - Added temporary input path management in runtime. - Refined worker functions to improve error handling and messaging. - Expanded tests to cover new functionality and ensure backward compatibility. - Fixed packaging script to align with versioning conventions.
This commit is contained in:
@@ -146,14 +146,14 @@ class AddonTest(unittest.TestCase):
|
||||
def test_conversion_dialog_exposes_requested_settings(self):
|
||||
operator = self.addon.ConvertImageToDepthPlane
|
||||
|
||||
self.assertEqual(
|
||||
operator.__annotations__["snapform_model"]["default"],
|
||||
"BASE",
|
||||
self.assertNotIn(
|
||||
"default",
|
||||
operator.__annotations__["snapform_model"],
|
||||
)
|
||||
self.assertTrue(operator.__annotations__["snapform_use_ray_pose"]["default"])
|
||||
self.assertEqual(
|
||||
operator.__annotations__["snapform_process_res"]["default"],
|
||||
1024,
|
||||
operator.__annotations__["snapform_process_res_factor"]["default"],
|
||||
0.5,
|
||||
)
|
||||
self.assertTrue(
|
||||
operator.__annotations__["snapform_delete_original"]["default"]
|
||||
@@ -165,14 +165,62 @@ class AddonTest(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
set(operator.__annotations__),
|
||||
{
|
||||
"snapform_depth_source",
|
||||
"snapform_displacement_via",
|
||||
"snapform_model",
|
||||
"snapform_use_ray_pose",
|
||||
"snapform_process_res",
|
||||
"snapform_process_res_factor",
|
||||
"snapform_delete_original",
|
||||
"snapform_subdivision_level",
|
||||
},
|
||||
)
|
||||
depth_sources = operator.__annotations__["snapform_depth_source"]
|
||||
self.assertEqual(depth_sources["default"], "ORIGINAL")
|
||||
self.assertEqual(
|
||||
[item[0] for item in depth_sources["items"]],
|
||||
["ESTIMATE", "ORIGINAL"],
|
||||
)
|
||||
self.assertEqual(depth_sources["items"][0][1], "AI Generated")
|
||||
self.assertEqual(depth_sources["name"], "Depth from")
|
||||
displacement_via = operator.__annotations__[
|
||||
"snapform_displacement_via"
|
||||
]
|
||||
self.assertEqual(displacement_via["default"], "MODIFIER")
|
||||
self.assertEqual(displacement_via["name"], "Displace via")
|
||||
self.assertEqual(
|
||||
[item[0] for item in displacement_via["items"]],
|
||||
["MODIFIER", "MATERIAL"],
|
||||
)
|
||||
draw_source = inspect.getsource(operator.draw)
|
||||
self.assertIn(
|
||||
'if self.snapform_depth_source == "ESTIMATE":',
|
||||
draw_source,
|
||||
)
|
||||
self.assertLess(
|
||||
draw_source.index(
|
||||
'"snapform_process_res_factor",'
|
||||
),
|
||||
draw_source.index(
|
||||
'layout.prop(self, "snapform_subdivision_level")'
|
||||
),
|
||||
)
|
||||
self.assertIn(
|
||||
'text=f"Resolution Scale ({process_res} px)"',
|
||||
draw_source,
|
||||
)
|
||||
self.assertIn("estimate_box = layout.box()", draw_source)
|
||||
self.assertIn(
|
||||
'estimate_box.prop(self, "snapform_model")',
|
||||
draw_source,
|
||||
)
|
||||
self.assertIn(
|
||||
'estimate_box.prop(self, "snapform_use_ray_pose")',
|
||||
draw_source,
|
||||
)
|
||||
self.assertNotIn("layout.separator", draw_source)
|
||||
self.assertNotIn("layout.label", draw_source)
|
||||
model_items = operator.__annotations__["snapform_model"]["items"]
|
||||
model_items = model_items(None, None)
|
||||
self.assertEqual(
|
||||
[item[0] for item in model_items],
|
||||
[
|
||||
@@ -184,6 +232,127 @@ class AddonTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
self.assertIn("invoke_props_dialog", inspect.getsource(operator.invoke))
|
||||
self.assertNotIn(
|
||||
'"DA3"',
|
||||
inspect.getsource(operator),
|
||||
)
|
||||
|
||||
def test_depth_conversion_progress_is_provider_neutral(self):
|
||||
starting_source = inspect.getsource(
|
||||
self.addon.RunJob._starting_message
|
||||
)
|
||||
finish_source = inspect.getsource(self.addon.RunJob._finish)
|
||||
|
||||
self.assertIn('return "Starting depth estimation"', starting_source)
|
||||
self.assertIn('"Depth plane is ready"', finish_source)
|
||||
self.assertNotIn("DA3 image depth", starting_source)
|
||||
self.assertNotIn("DA3 depth plane", finish_source)
|
||||
|
||||
def test_model_options_mark_only_models_that_are_not_downloaded(self):
|
||||
items_callback = self.addon.SnapFormSettings.__annotations__["model"][
|
||||
"items"
|
||||
]
|
||||
with patch.object(
|
||||
self.addon.runtime,
|
||||
"model_ready",
|
||||
side_effect=lambda key: key == "BASE",
|
||||
):
|
||||
items = items_callback(None, None)
|
||||
|
||||
labels = {item[0]: item[1] for item in items}
|
||||
self.assertEqual(labels["BASE"], "DA3 Base")
|
||||
self.assertEqual(labels["SMALL"], "DA3 Small (Missing)")
|
||||
|
||||
def test_packed_image_is_exported_without_unpacking(self):
|
||||
operator = self.addon.ConvertImageToDepthPlane()
|
||||
operator.snapform_depth_source = "ESTIMATE"
|
||||
operator.snapform_model = "BASE"
|
||||
operator.snapform_use_ray_pose = True
|
||||
operator.snapform_process_res_factor = 0.5
|
||||
operator.snapform_displacement_via = "MATERIAL"
|
||||
operator.snapform_subdivision_level = 5
|
||||
operator.snapform_delete_original = True
|
||||
image = SimpleNamespace(
|
||||
filepath="",
|
||||
packed_file=object(),
|
||||
size=(1920, 1080),
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
object=SimpleNamespace(name="Reference", data=image),
|
||||
scene=object(),
|
||||
)
|
||||
calls = []
|
||||
self.fake_bpy.ops = SimpleNamespace(
|
||||
snapform=SimpleNamespace(
|
||||
run_job=lambda **options: calls.append(options)
|
||||
or {"RUNNING_MODAL"},
|
||||
),
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as directory:
|
||||
packed_path = Path(directory) / "packed.png"
|
||||
packed_path.touch()
|
||||
with patch.object(
|
||||
self.addon.image_depth,
|
||||
"save_packed_image",
|
||||
return_value=packed_path,
|
||||
) as save_packed_image:
|
||||
result = operator.execute(context)
|
||||
|
||||
self.assertEqual(result, {"FINISHED"})
|
||||
save_packed_image.assert_called_once_with(image, context.scene)
|
||||
self.assertEqual(calls[0]["snapform_input_path"], str(packed_path))
|
||||
self.assertEqual(calls[0]["snapform_process_res"], 960)
|
||||
self.assertEqual(
|
||||
calls[0]["snapform_displacement_via"],
|
||||
"MATERIAL",
|
||||
)
|
||||
self.assertTrue(calls[0]["snapform_delete_input"])
|
||||
|
||||
def test_packed_image_export_writes_a_png_copy(self):
|
||||
class PackedImage:
|
||||
def save_render(self, filepath, scene):
|
||||
self.filepath = filepath
|
||||
self.scene = scene
|
||||
self.file_format = scene.render.image_settings.file_format
|
||||
Path(filepath).write_bytes(b"packed image")
|
||||
|
||||
image = PackedImage()
|
||||
image_settings = SimpleNamespace(file_format="OPEN_EXR")
|
||||
scene = SimpleNamespace(
|
||||
render=SimpleNamespace(image_settings=image_settings),
|
||||
)
|
||||
with TemporaryDirectory() as directory:
|
||||
output_path = Path(directory) / "input.png"
|
||||
with patch.object(
|
||||
self.addon.runtime,
|
||||
"temporary_input_path",
|
||||
return_value=output_path,
|
||||
):
|
||||
result = self.addon.image_depth.save_packed_image(image, scene)
|
||||
|
||||
self.assertEqual(result, output_path)
|
||||
self.assertEqual(output_path.read_bytes(), b"packed image")
|
||||
self.assertEqual(image.filepath, str(output_path))
|
||||
self.assertIs(image.scene, scene)
|
||||
self.assertEqual(image.file_format, "PNG")
|
||||
self.assertEqual(image_settings.file_format, "OPEN_EXR")
|
||||
|
||||
def test_temporary_packed_input_is_removed_when_job_closes(self):
|
||||
closed = []
|
||||
with TemporaryDirectory() as directory:
|
||||
input_path = Path(directory) / "packed.png"
|
||||
input_path.touch()
|
||||
self.addon.operators._active_job = SimpleNamespace(
|
||||
log_file=SimpleNamespace(close=lambda: closed.append(True)),
|
||||
temporary_input_path=input_path,
|
||||
)
|
||||
|
||||
self.addon.operators.close_active_job()
|
||||
|
||||
self.assertFalse(input_path.exists())
|
||||
self.assertEqual(closed, [True])
|
||||
self.assertIsNone(self.addon.operators.active_job())
|
||||
|
||||
def test_image_depth_model_and_ray_pose_are_independent(self):
|
||||
settings = SimpleNamespace(
|
||||
@@ -255,7 +424,7 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
reconstruct.call_args.kwargs["process_res_method"],
|
||||
"lower_bound_resize",
|
||||
"upper_bound_resize",
|
||||
)
|
||||
self.assertEqual(
|
||||
reconstruct.call_args.kwargs["use_ray_pose"],
|
||||
@@ -279,7 +448,7 @@ class AddonTest(unittest.TestCase):
|
||||
str(runtime.DEFAULT_STORAGE_ROOT),
|
||||
)
|
||||
|
||||
def test_process_res_method_defaults_to_lower_bound_resize(self):
|
||||
def test_process_res_method_defaults_to_upper_bound_resize(self):
|
||||
process_res = self.addon.SnapFormSettings.__annotations__[
|
||||
"process_res"
|
||||
]
|
||||
@@ -290,7 +459,7 @@ class AddonTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(process_res["default"], 1024)
|
||||
self.assertEqual(process_res_method["default"], "lower_bound_resize")
|
||||
self.assertEqual(process_res_method["default"], "upper_bound_resize")
|
||||
|
||||
def test_reconstruct_command_preserves_da3_parameter_names(self):
|
||||
runtime = importlib.import_module("snapform.runtime")
|
||||
@@ -404,7 +573,7 @@ class AddonTest(unittest.TestCase):
|
||||
gs_model = self.addon.SnapFormSettings.__annotations__[
|
||||
"gs_model"
|
||||
]
|
||||
identifiers = [item[0] for item in gs_model["items"]]
|
||||
identifiers = [item[0] for item in gs_model["items"](None, None)]
|
||||
|
||||
self.assertEqual(
|
||||
identifiers,
|
||||
@@ -422,13 +591,13 @@ class AddonTest(unittest.TestCase):
|
||||
self.addon.DebugPanel.draw
|
||||
)
|
||||
|
||||
self.assertEqual(draw_source.count("layout.operator("), 1)
|
||||
self.assertEqual(draw_source.count("export_row.operator("), 1)
|
||||
self.assertIn('settings.export_format == "glb"', draw_source)
|
||||
self.assertIn('settings.export_format == "gs_ply"', draw_source)
|
||||
self.assertIn('"gs_model" if settings.export_format', draw_source)
|
||||
self.assertIn("model_row.prop(settings, model_property)", draw_source)
|
||||
self.assertIn('model_row.label(text="", icon="CHECKMARK")', draw_source)
|
||||
self.assertIn('model_row.label(text="", icon="ERROR")', draw_source)
|
||||
self.assertIn("layout.prop(settings, model_property)", draw_source)
|
||||
self.assertNotIn("model_row", draw_source)
|
||||
self.assertIn("export_row.enabled = active_job() is None", draw_source)
|
||||
self.assertNotIn('text="Ready"', draw_source)
|
||||
self.assertNotIn('text="Missing"', draw_source)
|
||||
self.assertNotIn("License:", draw_source)
|
||||
@@ -444,6 +613,11 @@ class AddonTest(unittest.TestCase):
|
||||
|
||||
self.assertIn("for model in MODELS.values()", draw_source)
|
||||
self.assertIn("operator.model_key = model.key", model_source)
|
||||
self.assertIn(
|
||||
"download_row.enabled = runtime_ready() and not running",
|
||||
model_source,
|
||||
)
|
||||
self.assertNotIn('label(text="Not downloaded"', model_source)
|
||||
self.assertNotIn("Isolated Runtime ready", draw_source)
|
||||
self.assertNotIn("License:", model_source)
|
||||
self.assertEqual(
|
||||
@@ -451,6 +625,14 @@ class AddonTest(unittest.TestCase):
|
||||
"Setup",
|
||||
)
|
||||
|
||||
def test_preferences_install_button_is_disabled_while_busy(self):
|
||||
draw_source = inspect.getsource(self.addon.SnapFormPreferences.draw)
|
||||
|
||||
self.assertIn(
|
||||
"install_row.enabled = active_job() is None",
|
||||
draw_source,
|
||||
)
|
||||
|
||||
def test_completed_export_opens_job_directory(self):
|
||||
calls = []
|
||||
self.fake_bpy.ops = SimpleNamespace(
|
||||
|
||||
@@ -234,7 +234,7 @@ class ImageDepthTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(value, expected)
|
||||
|
||||
def test_source_empty_is_only_removed_when_requested(self):
|
||||
source = inspect.getsource(self.image_depth.build_depth_plane)
|
||||
source = inspect.getsource(self.image_depth.finish_depth_plane)
|
||||
|
||||
self.assertIn("source_name = source_object.name", source)
|
||||
self.assertIn("if delete_source:", source)
|
||||
@@ -243,27 +243,88 @@ class ImageDepthTest(unittest.TestCase):
|
||||
source,
|
||||
)
|
||||
self.assertNotIn("source_object.name =", source)
|
||||
self.assertIn(
|
||||
"bpy.data.objects.new(\n source_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))
|
||||
|
||||
self.assertEqual(
|
||||
self.image_depth.scaled_process_resolution(image, 0.5),
|
||||
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",
|
||||
data=SimpleNamespace(name="img.png"),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.image_depth.depth_data_name(source_object),
|
||||
"img.depth",
|
||||
)
|
||||
source_object.data.name = "img.png.001"
|
||||
self.assertEqual(
|
||||
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_data_name(source_object)", estimate_source)
|
||||
self.assertIn("depth_data_name(source_object)", original_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 = f"{source_name}.depth"', source)
|
||||
self.assertIn("depth_name = 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("bpy.data.textures.new(\n depth_name,", source)
|
||||
self.assertIn('texture.extension = "EXTEND"', source)
|
||||
|
||||
def test_original_image_drives_bump_instead_of_displacement(self):
|
||||
def test_material_supports_bump_and_optional_displacement(self):
|
||||
source = inspect.getsource(self.image_depth.create_material)
|
||||
|
||||
self.assertIn('nodes.new("ShaderNodeBump")', source)
|
||||
@@ -297,16 +358,94 @@ class ImageDepthTest(unittest.TestCase):
|
||||
'curve_points[-1].location = (1.0, 0.1)',
|
||||
source,
|
||||
)
|
||||
self.assertIn(
|
||||
'shader.inputs["IOR"].default_value = 1.2',
|
||||
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.assertNotIn("ShaderNodeDisplacement", 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_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,
|
||||
)
|
||||
|
||||
def test_surface_displacement_is_set_to_displacement_only(self):
|
||||
class Material:
|
||||
surface_displacement_method = "BUMP"
|
||||
|
||||
material = Material()
|
||||
self.image_depth.set_displacement_only(material)
|
||||
|
||||
self.assertEqual(
|
||||
material.surface_displacement_method,
|
||||
"DISPLACEMENT",
|
||||
)
|
||||
|
||||
def test_plane_transform_scales_depth_axis(self):
|
||||
source = inspect.getsource(self.image_depth.apply_plane_transform)
|
||||
|
||||
@@ -29,7 +29,7 @@ class PackTest(unittest.TestCase):
|
||||
finally:
|
||||
pack.DIST_DIR = original_dist
|
||||
|
||||
self.assertEqual(output_path.name, "SnapForm.v0.9.27.b45.zip")
|
||||
self.assertEqual(output_path.name, "SnapForm.v0.9.27.zip")
|
||||
with ZipFile(output_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class WorkerTest(unittest.TestCase):
|
||||
source = inspect.getsource(self.worker.reconstruct)
|
||||
|
||||
self.assertLess(
|
||||
source.index('"Starting DA3 runtime"'),
|
||||
source.index('"Starting AI runtime"'),
|
||||
source.index("import torch"),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user