feat(node_assets): add image plane geometry nodes and validation scripts
- Implemented `image_plane.py` to build the Image Plane Geometry Nodes group with customizable parameters for geometry, subdivide, and thickness. - Created `image_plane_validation.py` to validate the functionality of the image plane, ensuring correct geometry generation and material assignment. - Developed `material_layers.py` to construct shader node groups for image materials, including depth and normal handling. - Authored `plane_layouts.py` to define node arrangements for image geometry assets. - Introduced `shadeless.py` to create a shadeless shader node group that displays color without scene lighting while preserving shadows and reflections.
This commit is contained in:
473
tests/test_preferences.py
Normal file
473
tests/test_preferences.py
Normal file
@@ -0,0 +1,473 @@
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
|
||||
|
||||
class PreferencesTest(BlenderTestCase):
|
||||
def test_apple_silicon_architectures_support_coreml(self):
|
||||
properties = self.anyimage.properties
|
||||
for architecture in ("arm64", "aarch64"):
|
||||
with (
|
||||
self.subTest(architecture=architecture),
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value=architecture,
|
||||
),
|
||||
):
|
||||
self.assertEqual(
|
||||
properties.supported_devices(),
|
||||
["cpu", "coreml"],
|
||||
)
|
||||
|
||||
|
||||
def test_non_apple_silicon_mac_uses_cpu(self):
|
||||
properties = self.anyimage.properties
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="x86_64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
|
||||
|
||||
def test_manifest_coreml_requires_apple_silicon(self):
|
||||
properties = self.anyimage.properties
|
||||
manifest = {"devices": ["coreml", "cpu"]}
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value=manifest,
|
||||
),
|
||||
patch.object(properties.sys, "platform", "linux"),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value=manifest,
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="aarch64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(
|
||||
properties.supported_devices(),
|
||||
["cpu", "coreml"],
|
||||
)
|
||||
|
||||
|
||||
def test_manifest_rejects_old_mps_device(self):
|
||||
properties = self.anyimage.properties
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={"devices": ["mps", "cpu"]},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "darwin"),
|
||||
patch.object(
|
||||
properties.platform,
|
||||
"machine",
|
||||
return_value="arm64",
|
||||
),
|
||||
):
|
||||
self.assertEqual(properties.supported_devices(), ["cpu"])
|
||||
|
||||
|
||||
def test_windows_keeps_default_directml_device(self):
|
||||
properties = self.anyimage.properties
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.runtime,
|
||||
"environment_manifest",
|
||||
return_value={},
|
||||
),
|
||||
patch.object(properties.sys, "platform", "win32"),
|
||||
):
|
||||
self.assertEqual(
|
||||
properties.supported_devices(),
|
||||
["directml", "cpu"],
|
||||
)
|
||||
|
||||
|
||||
def test_geometry_model_choices_match_moge_catalog(self):
|
||||
properties = self.anyimage.properties
|
||||
expected = tuple(properties.shared_model_catalog.MOGE2_MODELS)
|
||||
geometry = properties.geometry_model_enum_items(None, None)
|
||||
self.assertEqual(tuple(item[0] for item in geometry), expected)
|
||||
|
||||
|
||||
def test_global_ai_input_size_defaults_and_clamps(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
2048,
|
||||
)
|
||||
preferences.max_ai_input_size = 10000
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
8192,
|
||||
)
|
||||
preferences.max_ai_input_size = 1
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_ai_input_size(),
|
||||
256,
|
||||
)
|
||||
|
||||
|
||||
def test_cutout_mesh_resolution_defaults_and_clamps_independently(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 2048
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 1
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
4,
|
||||
)
|
||||
|
||||
|
||||
def test_frame_resolution_defaults_and_clamps_independently(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 2048)
|
||||
preferences.max_frame_resolution = 10000
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 8192)
|
||||
preferences.max_frame_resolution = 1
|
||||
self.assertEqual(preference_module.configured_max_frame_resolution(), 64)
|
||||
|
||||
|
||||
def test_preferences_draws_upscale_model_download(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
downloads = []
|
||||
properties = []
|
||||
labels = []
|
||||
boxes = []
|
||||
|
||||
class Layout:
|
||||
enabled = True
|
||||
|
||||
def prop(self, _owner, name, **_options):
|
||||
properties.append(name)
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
def label(self, **_options):
|
||||
labels.append(_options.get("text"))
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def split(self, **_options):
|
||||
return self
|
||||
|
||||
def column(self, **_options):
|
||||
return self
|
||||
|
||||
def box(self):
|
||||
boxes.append(self)
|
||||
return self
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operator = SimpleNamespace(model="")
|
||||
if identifier == "anyimage.download_model":
|
||||
downloads.append(operator)
|
||||
return operator
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
preferences.geometry_model = "MOGE2_VITS_NORMAL"
|
||||
preferences.background_model = "BEN2_BASE"
|
||||
preferences.upscale_model = "SAFMN_REAL_X4"
|
||||
catalog = tuple(
|
||||
{
|
||||
"key": key,
|
||||
"label": key,
|
||||
"family": family,
|
||||
"enabled": True,
|
||||
"ready": False,
|
||||
}
|
||||
for key, family in (
|
||||
("MOGE2_VITS_NORMAL", "moge2"),
|
||||
("BEN2_BASE", "ben2"),
|
||||
("REALESRGAN_GENERAL_WDN_X4V3", "upscale"),
|
||||
("SAFMN_REAL_X4", "upscale"),
|
||||
)
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"ai_status",
|
||||
return_value={
|
||||
"environment_ready": True,
|
||||
"missing_models": (),
|
||||
"ready": True,
|
||||
},
|
||||
),
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"model_catalog",
|
||||
return_value=catalog,
|
||||
),
|
||||
patch.object(self.anyimage.runtime, "server_busy", return_value=False),
|
||||
):
|
||||
preferences.draw(None)
|
||||
|
||||
self.assertEqual(
|
||||
[download.model for download in downloads],
|
||||
["MOGE2_VITS_NORMAL", "BEN2_BASE", "SAFMN_REAL_X4"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
label
|
||||
for label in labels
|
||||
if label
|
||||
in {
|
||||
"Server",
|
||||
"AI",
|
||||
"Tools",
|
||||
"Frame Tool",
|
||||
"Depth & Normal",
|
||||
"Cutout Tool",
|
||||
}
|
||||
],
|
||||
[
|
||||
"Server",
|
||||
"AI",
|
||||
"Tools",
|
||||
"Frame Tool",
|
||||
"Depth & Normal",
|
||||
"Cutout Tool",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(boxes), 4)
|
||||
self.assertIn("Shading", labels)
|
||||
self.assertIn("adapt_material_to_view_transform", properties)
|
||||
adaptation = preference_module.AnyImagePreferences.__annotations__[
|
||||
"adapt_material_to_view_transform"
|
||||
]
|
||||
self.assertTrue(adaptation["default"])
|
||||
self.assertNotIn("update", adaptation)
|
||||
self.assertIn(preference_module.AnyImagePreferences, self.anyimage.CLASSES)
|
||||
self.assertNotIn("Normal Model", labels)
|
||||
self.assertIn("background_model", properties)
|
||||
self.assertIn("upscale_model", properties)
|
||||
self.assertIn("max_ai_input_size", properties)
|
||||
self.assertIn("max_frame_resolution", properties)
|
||||
self.assertIn("max_cutout_mesh_resolution", properties)
|
||||
self.assertIn("balloon_profile_algorithm", properties)
|
||||
balloon_profile = preference_module.AnyImagePreferences.__annotations__[
|
||||
"balloon_profile_algorithm"
|
||||
]
|
||||
self.assertEqual(balloon_profile["default"], "POISSON")
|
||||
self.assertEqual(
|
||||
tuple(item[0] for item in balloon_profile["items"]),
|
||||
("POISSON", "TEDDY"),
|
||||
)
|
||||
self.assertEqual(
|
||||
preference_module.AnyImagePreferences.__annotations__["upscale_model"][
|
||||
"name"
|
||||
],
|
||||
"Upscale Image Model",
|
||||
)
|
||||
self.assertEqual(
|
||||
preference_module.AnyImagePreferences.__annotations__["geometry_model"][
|
||||
"name"
|
||||
],
|
||||
"Generate Depth Map Model",
|
||||
)
|
||||
self.assertEqual(
|
||||
preference_module.AnyImagePreferences.__annotations__["background_model"][
|
||||
"name"
|
||||
],
|
||||
"Remove Background Model",
|
||||
)
|
||||
|
||||
|
||||
def test_ready_models_show_their_ai_feature(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
labels = []
|
||||
|
||||
class Layout:
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def label(self, **options):
|
||||
labels.append((options.get("text"), options.get("icon")))
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
model = {"ready": True}
|
||||
for ready_label in (
|
||||
"AI Generate Depth Map",
|
||||
"AI Remove Background",
|
||||
"AI Upscale Image",
|
||||
):
|
||||
preferences._draw_model_status(Layout(), model, ready_label)
|
||||
|
||||
self.assertEqual(
|
||||
labels,
|
||||
[
|
||||
("AI Generate Depth Map", "CHECKMARK"),
|
||||
("AI Remove Background", "CHECKMARK"),
|
||||
("AI Upscale Image", "CHECKMARK"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_preferences_hide_ai_until_server_is_installed(self):
|
||||
preference_module = importlib.import_module("anyimage.preferences")
|
||||
labels = []
|
||||
operators = []
|
||||
|
||||
class Layout:
|
||||
enabled = True
|
||||
|
||||
def box(self):
|
||||
return self
|
||||
|
||||
def label(self, **options):
|
||||
labels.append(options.get("text"))
|
||||
|
||||
def prop(self, *_args, **_options):
|
||||
return None
|
||||
|
||||
def row(self, **_options):
|
||||
return self
|
||||
|
||||
def separator(self, **_options):
|
||||
return None
|
||||
|
||||
def operator(self, identifier, **_options):
|
||||
operators.append(identifier)
|
||||
return SimpleNamespace(model="")
|
||||
|
||||
preferences = preference_module.AnyImagePreferences()
|
||||
preferences.layout = Layout()
|
||||
with (
|
||||
patch.object(
|
||||
self.anyimage.properties,
|
||||
"ai_status",
|
||||
return_value={
|
||||
"environment_ready": False,
|
||||
"missing_models": (),
|
||||
"ready": False,
|
||||
},
|
||||
),
|
||||
patch.object(self.anyimage.properties, "model_catalog", return_value=()),
|
||||
patch.object(self.anyimage.runtime, "server_busy", return_value=False),
|
||||
):
|
||||
preferences.draw(None)
|
||||
|
||||
self.assertEqual(
|
||||
labels,
|
||||
["Server", "Shading", "Tools", "Frame Tool", "Depth & Normal", "Cutout Tool"],
|
||||
)
|
||||
self.assertEqual(
|
||||
operators,
|
||||
["anyimage.setup_ai_environment", "anyimage.open_server_log"],
|
||||
)
|
||||
|
||||
|
||||
def test_depth_plane_request_includes_the_global_ai_input_limit(self):
|
||||
operator = self.convert_to_plane.GenerateDepthPlane()
|
||||
operator.input_path = __file__
|
||||
operator.analysis_input_path = __file__
|
||||
operator.source_object_name = "Source"
|
||||
operator.mesh_detail = 0
|
||||
operator.model = self.anyimage.properties.DEFAULT_MOGE2_MODEL_KEY
|
||||
operator.resolution_level = 5
|
||||
operator.max_input_size = 1536
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1))
|
||||
)
|
||||
with (
|
||||
patch.object(self.convert_to_plane, "require_environment"),
|
||||
patch.object(self.convert_to_plane, "require_model"),
|
||||
patch.object(self.convert_to_plane, "production_device", return_value="CPU"),
|
||||
patch.object(self.convert_to_plane, "source_start_frame", return_value=1),
|
||||
):
|
||||
parameters = operator.request(context)
|
||||
|
||||
self.assertEqual(parameters["max_input_size"], 1536)
|
||||
self.assertNotIn("shape", parameters)
|
||||
self.assertNotIn("polygon", parameters)
|
||||
|
||||
|
||||
def test_upscale_execute_reads_model_and_size_from_preferences(self):
|
||||
source_image = SimpleNamespace(
|
||||
name="Original",
|
||||
source="FILE",
|
||||
anyimage_upscale_source=None,
|
||||
)
|
||||
source_object = SimpleNamespace(
|
||||
name="Source",
|
||||
type="EMPTY",
|
||||
empty_display_type="IMAGE",
|
||||
data=source_image,
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
object=source_object,
|
||||
scene=SimpleNamespace(anyimage_settings=SimpleNamespace(video_frames=1)),
|
||||
)
|
||||
operator = self.upscale.UpscaleImage()
|
||||
with (
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"configured_upscale_model",
|
||||
return_value="SAFMN_REAL_X4",
|
||||
),
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"configured_max_ai_input_size",
|
||||
return_value=1024,
|
||||
),
|
||||
patch.object(
|
||||
self.upscale,
|
||||
"prepare_image_input",
|
||||
return_value=(Path("input.png"), False),
|
||||
),
|
||||
patch.object(
|
||||
self.runtime.JobOperatorBase,
|
||||
"execute",
|
||||
return_value={"RUNNING_MODAL"},
|
||||
),
|
||||
):
|
||||
result = operator.execute(context)
|
||||
|
||||
self.assertEqual(result, {"RUNNING_MODAL"})
|
||||
self.assertEqual(operator.model, "SAFMN_REAL_X4")
|
||||
self.assertEqual(operator.max_input_size, 1024)
|
||||
Reference in New Issue
Block a user