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(
|
||||
|
||||
Reference in New Issue
Block a user