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