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:
2026-08-03 13:47:15 +08:00
parent b02372c457
commit a503786c62
14 changed files with 827 additions and 174 deletions

View File

@@ -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(