Implement Balloon Depth Sampling and Validation Tools

- Added depth.py for MoGe-2 depth sampling and balloon deformation functionality.
- Introduced validate_balloon_asset.py to validate balloon shape modifiers and material properties in Blender.
- Enhanced test_server.py with new unit tests for balloon depth analysis and job progress tracking.
- Updated existing tests to improve code clarity and maintainability.
This commit is contained in:
2026-08-21 08:27:53 +08:00
parent 8afcf48ddf
commit b5a2f9690c
17 changed files with 1952 additions and 281 deletions

View File

@@ -82,34 +82,25 @@ class AddonTest(unittest.TestCase):
cls.addon = importlib.import_module("snapform")
cls.job_runtime = importlib.import_module("snapform.runtime")
cls.model_operators = importlib.import_module("snapform.operators.models")
cls.image_depth = importlib.import_module(
"snapform.operators.material"
)
cls.image_data = importlib.import_module(
"snapform.operators.image"
)
cls.image_depth = importlib.import_module("snapform.operators.material")
cls.image_data = importlib.import_module("snapform.operators.image")
cls.depth_operator = importlib.import_module(
"snapform.operators.depth_plane.operator"
)
cls.depth_build = importlib.import_module(
"snapform.operators.depth_plane.build"
)
cls.image_background = importlib.import_module(
"snapform.operators.background"
)
cls.image_mesh = importlib.import_module(
"snapform.operators.surface"
)
cls.image_background = importlib.import_module("snapform.operators.background")
cls.image_mesh = importlib.import_module("snapform.operators.surface")
cls.balloon = importlib.import_module("snapform.operators.balloon.operator")
cls.balloon_geometry = importlib.import_module(
"snapform.operators.balloon.geometry"
)
cls.balloon_depth = importlib.import_module("snapform.operators.balloon.depth")
cls.balloon_poisson = importlib.import_module(
"snapform.operators.balloon.poisson"
)
cls.balloon_teddy = importlib.import_module(
"snapform.operators.balloon.teddy"
)
cls.balloon_teddy = importlib.import_module("snapform.operators.balloon.teddy")
cls.paths_after_import = tuple(sys.path)
cls.fake_bpy = fake_bpy
@@ -298,7 +289,9 @@ class AddonTest(unittest.TestCase):
window_manager = SimpleNamespace(
progress_begin=lambda *_args: None,
progress_update=lambda _value: None,
event_timer_add=lambda *_args, **_kwargs: timers.append(object()) or timers[-1],
event_timer_add=lambda *_args, **_kwargs: (
timers.append(object()) or timers[-1]
),
event_timer_remove=lambda _timer: None,
modal_handler_add=lambda _operator: None,
)
@@ -320,7 +313,9 @@ class AddonTest(unittest.TestCase):
with patch.object(runtime, "storage_root_factory", root):
self.assertEqual(runtime.storage_root(), root.resolve())
self.assertEqual(runtime.environment_directory(), root / ".venv")
self.assertEqual(runtime.install_status_path(), root / "install-status.json")
self.assertEqual(
runtime.install_status_path(), root / "install-status.json"
)
self.assertEqual(runtime.install_log_path(), root / "install.log")
self.assertEqual(runtime.server_log_path(), root / "server.log")
@@ -328,8 +323,10 @@ class AddonTest(unittest.TestCase):
runtime = self.addon.runtime
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
python = root / ".venv" / (
"Scripts/python.exe" if sys.platform == "win32" else "bin/python"
python = (
root
/ ".venv"
/ ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
)
python.parent.mkdir(parents=True)
python.touch()
@@ -370,9 +367,7 @@ class AddonTest(unittest.TestCase):
def test_snapform_post_install_is_ordinary_code_using_runtime_request(self):
calls = []
fake_runtime = SimpleNamespace(
request=lambda job_type, parameters: calls.append(
(job_type, parameters)
)
request=lambda job_type, parameters: calls.append((job_type, parameters))
)
self.job_runtime.post_install(fake_runtime)
self.assertEqual(
@@ -394,9 +389,7 @@ class AddonTest(unittest.TestCase):
RuntimeError,
"MoGe-2 ViT-S Normal: network unavailable; BEN2 Base: network unavailable",
):
self.job_runtime.post_install(
SimpleNamespace(request=request)
)
self.job_runtime.post_install(SimpleNamespace(request=request))
self.assertEqual(calls, ["MOGE2_VITS_NORMAL", "BEN2_BASE"])
@@ -414,14 +407,18 @@ class AddonTest(unittest.TestCase):
controller = runtime_module.EnvironmentController(runtime)
controller.process = SimpleNamespace(poll=lambda: 0, returncode=0)
with tempfile.TemporaryDirectory() as directory, patch.object(
runtime,
"post_install",
post_install,
), patch.object(
runtime,
"install_status_path",
return_value=Path(directory) / "missing.json",
with (
tempfile.TemporaryDirectory() as directory,
patch.object(
runtime,
"post_install",
post_install,
),
patch.object(
runtime,
"install_status_path",
return_value=Path(directory) / "missing.json",
),
):
self.assertEqual(controller.status("environment")["state"], "running")
self.assertTrue(started.wait(1.0))
@@ -441,14 +438,18 @@ class AddonTest(unittest.TestCase):
controller = runtime_module.EnvironmentController(runtime)
controller.process = SimpleNamespace(poll=lambda: 0, returncode=0)
with tempfile.TemporaryDirectory() as directory, patch.object(
runtime,
"post_install",
post_install,
), patch.object(
runtime,
"install_status_path",
return_value=Path(directory) / "missing.json",
with (
tempfile.TemporaryDirectory() as directory,
patch.object(
runtime,
"post_install",
post_install,
),
patch.object(
runtime,
"install_status_path",
return_value=Path(directory) / "missing.json",
),
):
controller.status("environment")
controller.post_thread.join(1.0)
@@ -477,17 +478,31 @@ class AddonTest(unittest.TestCase):
def test_conversion_model_lists_default_to_moge2_vits(self):
properties = importlib.import_module("snapform.properties")
catalog = (
{"key": "SMALL", "family": "da3", "enabled": True,
"label": "DA3 Small", "description": "DA3", "license": "Apache"},
{"key": "MOGE2_VITS_NORMAL", "family": "moge2", "enabled": True,
"label": "MoGe-2 ViT-S", "description": "MoGe", "license": "MIT"},
{
"key": "SMALL",
"family": "da3",
"enabled": True,
"label": "DA3 Small",
"description": "DA3",
"license": "Apache",
},
{
"key": "MOGE2_VITS_NORMAL",
"family": "moge2",
"enabled": True,
"label": "MoGe-2 ViT-S",
"description": "MoGe",
"license": "MIT",
},
)
with patch.object(properties, "model_catalog", return_value=catalog):
depth_items = properties.depth_plane_model_enum_items(None, None)
surface_items = properties.surface_model_enum_items(None, None)
balloon_items = properties.balloon_depth_model_enum_items(None, None)
self.assertEqual(depth_items[0][0], "MOGE2_VITS_NORMAL")
self.assertEqual(surface_items[0][0], "MOGE2_VITS_NORMAL")
self.assertEqual([item[0] for item in balloon_items], ["MOGE2_VITS_NORMAL"])
def test_required_model_operator_requests_both_models(self):
operator = self.addon.operators.DownloadRequiredModels()
@@ -521,13 +536,16 @@ class AddonTest(unittest.TestCase):
menu = self.addon.SnapFormImageMenu()
menu.layout = Layout()
with patch.object(
self.addon.runtime,
"environment_ready",
return_value=True,
), patch(
"snapform.properties.required_models_ready",
return_value=False,
with (
patch.object(
self.addon.runtime,
"environment_ready",
return_value=True,
),
patch(
"snapform.properties.required_models_ready",
return_value=False,
),
):
menu.draw(None)
@@ -565,13 +583,16 @@ class AddonTest(unittest.TestCase):
menu = self.addon.SnapFormImageMenu()
menu.layout = Layout()
with patch.object(
self.addon.runtime,
"environment_ready",
return_value=False,
), patch(
"snapform.properties.required_models_ready",
return_value=False,
with (
patch.object(
self.addon.runtime,
"environment_ready",
return_value=False,
),
patch(
"snapform.properties.required_models_ready",
return_value=False,
),
):
menu.draw(None)
@@ -617,20 +638,18 @@ class AddonTest(unittest.TestCase):
import numpy as np
polygon = np.asarray(
((100, 100), (900, 100), (950, 500), (800, 900),
(200, 900), (50, 500)),
((100, 100), (900, 100), (950, 500), (800, 900), (200, 900), (50, 500)),
dtype=float,
)
contour, interior, _spacing = (
self.balloon_geometry.prepare_planar_points(
polygon,
(1000, 1000),
(0.0, 1.0, 0.0, 1.0),
)
contour, interior, _spacing = self.balloon_geometry.prepare_planar_points(
polygon,
(1000, 1000),
(0.0, 1.0, 0.0, 1.0),
)
self.assertLess(len(contour), 150)
self.assertLess(len(interior), 400)
def test_balloon_sampling_uses_world_distance(self):
import numpy as np
@@ -715,6 +734,66 @@ class AddonTest(unittest.TestCase):
self.assertGreater(wide_height, narrow_height)
def test_poisson_balloon_supports_resolution_002(self):
import numpy as np
spacing = 0.02
size = 51
axis = np.arange(size, dtype=float) * spacing
points = np.asarray(
[(x, y) for y in axis for x in axis],
dtype=float,
)
faces = []
for row in range(size - 1):
for column in range(size - 1):
bottom_left = row * size + column
bottom_right = bottom_left + 1
top_left = bottom_left + size
top_right = top_left + 1
faces.append((bottom_left, bottom_right, top_right))
faces.append((bottom_left, top_right, top_left))
boundary = np.zeros(len(points), dtype=bool)
boundary[:size] = True
boundary[-size:] = True
boundary[::size] = True
boundary[size - 1 :: size] = True
heights = self.balloon_poisson.poisson_balloon_heights(
points,
faces,
boundary,
0.45,
)
self.assertTrue(np.isfinite(heights).all())
self.assertTrue((heights[boundary] == 0.0).all())
self.assertGreater(heights[(size * size) // 2], 0.0)
def test_poisson_solver_has_no_absolute_scale_cutoff(self):
import numpy as np
points = np.asarray(
((0, 0), (2, 0), (2, 2), (0, 2), (1, 1)),
dtype=float,
)
faces = ((0, 1, 4), (1, 2, 4), (2, 3, 4), (3, 0, 4))
boundary = np.asarray((True, True, True, True, False))
original = self.balloon_poisson.poisson_balloon_heights(
points,
faces,
boundary,
0.45,
)
tiny = self.balloon_poisson.poisson_balloon_heights(
points * 1e-5,
faces,
boundary,
0.45,
)
self.assertAlmostEqual(tiny[4], original[4] * 1e-5, places=12)
def test_teddy_balloon_uses_chordal_axis_radius(self):
import numpy as np
@@ -742,15 +821,18 @@ class AddonTest(unittest.TestCase):
points = np.asarray(((0, 0),), dtype=float)
boundary = np.asarray((True,))
with patch.object(
self.balloon_geometry,
"poisson_balloon_heights",
return_value=np.asarray((1.0,)),
) as poisson, patch.object(
self.balloon_geometry,
"teddy_balloon_heights",
return_value=np.asarray((2.0,)),
) as teddy:
with (
patch.object(
self.balloon_geometry,
"poisson_balloon_heights",
return_value=np.asarray((1.0,)),
) as poisson,
patch.object(
self.balloon_geometry,
"teddy_balloon_heights",
return_value=np.asarray((2.0,)),
) as teddy,
):
poisson_result = self.balloon_geometry.balloon_heights(
"POISSON", points, (), points, boundary, 0.45
)
@@ -762,6 +844,7 @@ class AddonTest(unittest.TestCase):
self.assertEqual(teddy_result[0], 2.0)
poisson.assert_called_once()
teddy.assert_called_once()
def test_balloon_geometry_has_a_shared_closed_seam(self):
from collections import Counter
@@ -801,29 +884,204 @@ class AddonTest(unittest.TestCase):
self.addon.CLASSES.index(self.balloon.ActivateBalloonTool),
)
def test_balloon_ai_refine_is_registered_and_forwarded_to_ben2(self):
def test_balloon_ai_analysis_is_registered_and_forwarded(self):
import inspect
self.assertIn(self.balloon.RefineBalloon, self.addon.CLASSES)
self.assertIn(self.balloon.AnalyzeBalloon, self.addon.CLASSES)
operator = self.balloon.ExtractBalloon
self.assertIn("ai_refine", operator.__annotations__)
self.assertIn("contour_analysis", operator.__annotations__)
self.assertIn("depth_analysis", operator.__annotations__)
self.assertIn("depth_model", operator.__annotations__)
settings = self.addon.properties.SnapFormSettings.__annotations__
self.assertIn("balloon_ai_refine", settings)
self.assertFalse(settings["balloon_ai_refine"]["default"])
for name in (
"balloon_contour_analysis",
"balloon_depth_analysis",
):
self.assertIn(name, settings)
self.assertFalse(settings[name]["default"])
self.assertIn("balloon_depth_model", settings)
self.assertEqual(settings["balloon_contour_analysis"]["name"], "Contour")
self.assertEqual(settings["balloon_depth_analysis"]["name"], "Depth")
execute_source = inspect.getsource(operator.execute)
invoke_source = inspect.getsource(operator.invoke)
request_source = inspect.getsource(self.balloon.RefineBalloon.request)
response_source = inspect.getsource(self.balloon.RefineBalloon.response)
request_source = inspect.getsource(self.balloon.AnalyzeBalloon.request)
response_source = inspect.getsource(self.balloon.AnalyzeBalloon.response)
tool_source = inspect.getsource(self.balloon.BalloonTool.draw_settings)
self.assertIn("bpy.ops.snapform.refine_balloon", execute_source)
self.assertIn("bpy.ops.snapform.analyze_balloon", execute_source)
self.assertIn("prepare_image_input(", execute_source)
self.assertIn("settings.balloon_ai_refine", invoke_source)
self.assertIn(
"settings.balloon_contour_analysis",
invoke_source,
)
self.assertIn(
"settings.balloon_depth_analysis",
invoke_source,
)
self.assertIn("self.depth_model = settings.balloon_depth_model", invoke_source)
self.assertIn("require_model(BEN2_MODEL_KEY)", request_source)
self.assertIn(
"require_model(self.depth_model)",
request_source,
)
self.assertIn('"polygon": polygon', request_source)
self.assertIn('result.file("contour")', response_source)
self.assertIn("extract_balloon_object(", response_source)
self.assertIn('result.file("depth")', response_source)
self.assertIn("depth_data=depth_data", response_source)
self.assertIn('push_undo("Extract Balloon")', response_source)
self.assertIn('"balloon_ai_refine"', tool_source)
self.assertIn('"balloon_contour_analysis"', tool_source)
self.assertIn('"balloon_depth_analysis"', tool_source)
self.assertIn('"balloon_depth_model"', tool_source)
def test_balloon_analysis_requires_only_enabled_models(self):
operator = self.balloon.AnalyzeBalloon()
operator.input_path = "input.png"
operator.selection_data = "[[0,0],[2,0],[0,2]]"
operator.resolution_level = 5
operator.depth_model = "MOGE2_VITB_NORMAL"
required = []
with (
patch.object(self.balloon, "require_environment"),
patch.object(
self.balloon,
"require_model",
side_effect=required.append,
),
patch.object(
self.balloon,
"production_device",
return_value="CUDA",
),
):
operator.contour_analysis = True
operator.depth_analysis = False
contour_request = operator.request(None)
operator.contour_analysis = False
operator.depth_analysis = True
depth_request = operator.request(None)
self.assertEqual(
required,
[
self.addon.properties.BEN2_MODEL_KEY,
"MOGE2_VITB_NORMAL",
],
)
self.assertTrue(contour_request["contour_analysis"])
self.assertFalse(contour_request["depth_analysis"])
self.assertFalse(depth_request["contour_analysis"])
self.assertTrue(depth_request["depth_analysis"])
self.assertEqual(depth_request["depth_model"], "MOGE2_VITB_NORMAL")
def test_balloon_depth_fields_and_shape_nodes(self):
import inspect
import numpy as np
depth_data = {
"depth_signal": np.asarray(
((1.0, -2.0), (0.5, 0.0)),
dtype=np.float32,
),
"baseline_vector": np.zeros(3, dtype=np.float32),
"valid": np.ones((2, 2), dtype=bool),
"origin": (0, 0),
"crop_size": (2, 2),
"source_size": (2, 2),
}
vertices = (
(0.0, 0.0, 2.0),
(0.0, 0.0, 2.0),
(0.0, 0.0, -2.0),
(0.0, 0.0, -2.0),
)
uv = ((0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0))
values, local_plane = self.balloon_depth.balloon_depth_fields(
vertices,
uv,
depth_data,
)
np.testing.assert_allclose(values, (1.5, -4.5, 0.5, -0.5))
self.assertAlmostEqual(float(np.median(values)), 0.0)
np.testing.assert_allclose(local_plane, (0.0, 0.0, 0.0))
self.assertEqual(
self.balloon.orient_balloon_vertices(((1.0, 2.0, 3.0),)),
[(-3.0, -1.0, 2.0)],
)
restored = np.asarray(self.balloon.BALLOON_AXIS_TO_SOURCE) @ np.asarray(
(-3.0, -1.0, 2.0, 1.0)
)
np.testing.assert_allclose(restored, (1.0, 2.0, 3.0, 1.0))
self.assertEqual(
self.balloon.orient_balloon_base_plane((0.2, 0.3, 0.4)),
(0.2, -0.3, -0.4),
)
source = inspect.getsource(self.balloon_depth.balloon_shape_node_group)
controller_source = inspect.getsource(
self.balloon_depth.create_balloon_base_plane
)
self.assertIn('"Thickness"', source)
self.assertIn('"Depth Strength"', source)
self.assertNotIn('"Far Clamp"', source)
self.assertIn('"Double Sided Depth"', source)
self.assertIn('"Base Plane"', source)
self.assertIn('nodes.new("GeometryNodeObjectInfo")', source)
self.assertIn("controller.parent = balloon", controller_source)
self.assertIn(
'controller.empty_display_type = "SINGLE_ARROW"', controller_source
)
self.assertIn('profile_factor.operation = "ABSOLUTE"', source)
self.assertIn('profile_statistics.data_type = "FLOAT"', source)
self.assertIn('profile_max_safe.operation = "MAXIMUM"', source)
self.assertIn('profile_normalized.operation = "DIVIDE"', source)
self.assertIn('profile_smooth.interpolation_type = "SMOOTHERSTEP"', source)
self.assertIn('profile_active.operation = "MULTIPLY"', source)
self.assertIn('effective_normal.input_type = "VECTOR"', source)
self.assertNotIn("o_balloon_depth_mix", source)
self.assertIn('front.operation = "LESS_THAN"', source)
self.assertIn('depth_residual.operation = "ADD"', source)
self.assertIn('depth_calibration.inputs[1].default_value = 0.5', source)
self.assertIn('corrected_depth.operation = "ADD"', source)
self.assertIn(
'profile_statistics.outputs["Max"], corrected_depth.inputs[1]',
source,
)
self.assertNotIn("baseline_thickness", source)
self.assertIn('separate.outputs["X"], base_scale.inputs[0]', source)
self.assertIn('double_choice.outputs["Output"], combine.inputs["X"]', source)
self.assertIn('depth_mix = nodes.new("ShaderNodeMix")', source)
self.assertIn('depth_mix.data_type = "FLOAT"', source)
self.assertIn('depth_mix.clamp_factor = True', source)
self.assertIn('base_magnitude.outputs[0], depth_mix.inputs["A"]', source)
self.assertIn('depth_scale.outputs[0], depth_mix.inputs["B"]', source)
self.assertNotIn("profile_inverse", source)
self.assertNotIn("thickness_component", source)
self.assertNotIn("depth_component", source)
self.assertNotIn("blended_magnitude", source)
self.assertIn('plane_offset.operation = "DOT_PRODUCT"', source)
self.assertIn("controller.location = (0.0, 0.0, 0.0)", controller_source)
self.assertNotIn("clamp_distance", source)
self.assertNotIn("safety_clamped", source)
self.assertNotIn("crossing_choice", source)
self.assertIn('front_choice.input_type = "FLOAT"', source)
self.assertIn('double_choice.input_type = "FLOAT"', source)
self.assertNotIn("double_inverse", source)
self.assertNotIn("depth_limited", source)
self.assertNotIn("side_gate", source)
self.assertIn('base_frame.label = "Base Thickness"', source)
self.assertIn('plane_frame.label = "Middle Plane"', source)
self.assertIn('mix_frame.label = "Depth Mix"', source)
self.assertIn('output_frame.label = "Side Output"', source)
self.assertIn('side.operation = "SIGN"', source)
self.assertIn("BALLOON_DEPTH_ATTRIBUTE", source)
self.assertEqual(
self.balloon_depth.BALLOON_DEPTH_ATTRIBUTE,
"o_balloon_depth",
)
def test_balloon_redo_properties_use_mode_and_meter_resolution(self):
import inspect
@@ -949,8 +1207,7 @@ class AddonTest(unittest.TestCase):
import json
matrix = tuple(
tuple(row * 4 + column for column in range(4))
for row in range(4)
tuple(row * 4 + column for column in range(4)) for row in range(4)
)
values = json.loads(self.balloon.serialize_matrix(matrix))
@@ -962,34 +1219,57 @@ class AddonTest(unittest.TestCase):
source = inspect.getsource(self.balloon.extract_balloon_object)
self.assertNotIn("objects.remove(source_object", source)
self.assertIn("balloon_name = source_name", source)
self.assertIn("selected_object = balloon if select_result else source_object", source)
self.assertIn("balloon.matrix_world = matrix_world.copy()", source)
self.assertIn(
"selected_object = balloon if select_result else source_object", source
)
self.assertIn(
"balloon.matrix_world = orient_balloon_world_matrix(matrix_world)",
source,
)
self.assertIn("create_material(", source)
self.assertIn("material_name = material_data_name(source_object)", source)
self.assertIn(
"reusable_balloon_material(material_name, color_name)",
"normal_required=uses_normal",
source,
)
self.assertIn("mark_balloon_material(material, image, normal_image)", source)
self.assertIn("merge_balloon_normal_image(normal_image, update_normal)", source)
self.assertIn("if reuse_material", source)
self.assertIn("color_image = image.copy()", source)
self.assertIn("color_image.name = color_name", source)
self.assertIn(
"material_name,\n color_image,\n 0.0,",
"material_name,\n color_image,\n 1.0,",
source,
)
self.assertIn(
"surface_group_name=TEXTURE_MATERIAL_NODE_GROUP_NAME",
"else TEXTURE_MATERIAL_NODE_GROUP_NAME",
source,
)
self.assertIn("owns_material", source)
self.assertIn("owns_color_image", source)
self.assertIn("balloon_depth_fields(", source)
self.assertIn("assign_balloon_shape_attributes(", source)
self.assertIn("inflation=INFLATION", source)
self.assertIn("create_balloon_base_plane(", source)
self.assertIn(
"add_balloon_shape_modifier(balloon, thickness, base_plane)", source
)
self.assertIn("BALLOON_MATERIAL_NODE_GROUP_NAME", source)
self.assertIn("normal_path=", source)
self.assertEqual(
self.image_depth.TEXTURE_MATERIAL_NODE_GROUP_NAME,
"O Texture Layer",
)
def test_balloon_material_reuse_requires_matching_image_and_node_group(self):
class MarkedMaterial(dict):
def __init__(self, name, **properties):
super().__init__(properties)
self.name = name
self.node_tree = None
color_image = SimpleNamespace(name="cow.color")
other_image = SimpleNamespace(name="other")
texture_node = SimpleNamespace(type="TEX_IMAGE", image=color_image)
texture_layer = SimpleNamespace(
type="GROUP",
@@ -999,6 +1279,23 @@ class AddonTest(unittest.TestCase):
name="cow.001",
node_tree=SimpleNamespace(nodes=[texture_node, texture_layer]),
)
marked = MarkedMaterial(
"unrelated-name",
snapform_balloon_material=True,
snapform_source_image=color_image,
)
marked_other = MarkedMaterial(
"cow",
snapform_balloon_material=True,
snapform_source_image=other_image,
)
normal_image = SimpleNamespace(name="cow.normal")
marked_normal = MarkedMaterial(
"depth-material",
snapform_balloon_material=True,
snapform_source_image=color_image,
snapform_balloon_normal_image=normal_image,
)
wrong_group = SimpleNamespace(
name="cow",
node_tree=SimpleNamespace(
@@ -1013,14 +1310,37 @@ class AddonTest(unittest.TestCase):
)
self.fake_bpy.data = SimpleNamespace(
objects=SimpleNamespace(get=lambda _name: None),
materials=[wrong_group, compatible],
materials=[
marked_other,
wrong_group,
compatible,
marked_normal,
marked,
],
images=[color_image],
)
self.assertIs(
self.balloon.reusable_balloon_material(
"cow",
"cow.color",
color_image,
),
marked,
)
self.assertIs(
self.balloon.reusable_balloon_material("cow", "cow.color"),
compatible,
)
self.assertIs(
self.balloon.reusable_balloon_material(
"cow",
"cow.color",
color_image,
normal_required=True,
),
marked_normal,
)
self.assertIs(
self.balloon.reusable_color_image("cow.color"),
color_image,
@@ -1056,31 +1376,40 @@ class AddonTest(unittest.TestCase):
(True, True, True),
)
for environment_ready, models_ready, expected in states:
with self.subTest(
environment_ready=environment_ready,
models_ready=models_ready,
expected=expected,
), patch.object(
self.addon.runtime,
"environment_ready",
return_value=environment_ready,
), patch.object(
self.addon.runtime,
"server_busy",
return_value=False,
), patch(
"snapform.properties.required_models_ready",
return_value=models_ready,
with (
self.subTest(
environment_ready=environment_ready,
models_ready=models_ready,
expected=expected,
),
patch.object(
self.addon.runtime,
"environment_ready",
return_value=environment_ready,
),
patch.object(
self.addon.runtime,
"server_busy",
return_value=False,
),
patch(
"snapform.properties.required_models_ready",
return_value=models_ready,
),
):
for module, operator in operators:
with self.subTest(operator=operator.__name__), patch.object(
module,
"is_image_empty",
return_value=True,
), patch.object(
module,
"video_total_frames",
return_value=1,
with (
self.subTest(operator=operator.__name__),
patch.object(
module,
"is_image_empty",
return_value=True,
),
patch.object(
module,
"video_total_frames",
return_value=1,
),
):
self.assertEqual(operator.poll(context), expected)
@@ -1095,18 +1424,20 @@ class AddonTest(unittest.TestCase):
(moge / "model.onnx").touch()
(ben2 / "model_fp16.onnx").touch()
with patch.object(
self.addon.runtime,
"storage_root",
return_value=root,
), patch.object(
self.addon.runtime,
"resource",
side_effect=AssertionError("UI must not start the Server"),
with (
patch.object(
self.addon.runtime,
"storage_root",
return_value=root,
),
patch.object(
self.addon.runtime,
"resource",
side_effect=AssertionError("UI must not start the Server"),
),
):
self.assertTrue(properties.required_models_ready())
def test_runtime_request_uses_request_response_shape(self):
result = object()
responses = []
@@ -1224,13 +1555,12 @@ class AddonTest(unittest.TestCase):
operator.resolution_level = 6
operator.remove_background = False
settings = SimpleNamespace(video_frames=12)
context = SimpleNamespace(
scene=SimpleNamespace(snapform_settings=settings)
)
context = SimpleNamespace(scene=SimpleNamespace(snapform_settings=settings))
with patch.object(
self.depth_operator, "require_environment"
), patch.object(self.depth_operator, "require_model"):
with (
patch.object(self.depth_operator, "require_environment"),
patch.object(self.depth_operator, "require_model"),
):
parameters = operator.request(context)
self.assertEqual(parameters["input"], __file__)
@@ -1247,9 +1577,7 @@ class AddonTest(unittest.TestCase):
result = object()
context = object()
with patch.object(
self.depth_operator, "build_moge2_depth_response"
) as build:
with patch.object(self.depth_operator, "build_moge2_depth_response") as build:
operator.response(context, result)
build.assert_called_once_with(
@@ -1280,8 +1608,9 @@ class AddonTest(unittest.TestCase):
calls = []
self.fake_bpy.ops = SimpleNamespace(
snapform=SimpleNamespace(
generate_moge2_depth=lambda **options: calls.append(options)
or {"RUNNING_MODAL"}
generate_moge2_depth=lambda **options: (
calls.append(options) or {"RUNNING_MODAL"}
)
)
)
@@ -1296,5 +1625,6 @@ class AddonTest(unittest.TestCase):
self.assertEqual(calls[0]["source_object_name"], "Reference")
self.assertFalse(any(key.startswith("snapform_") for key in calls[0]))
if __name__ == "__main__":
unittest.main()