- Replaced the Mesh Detail parameter with a new Edge Length property to define cutout sampling in physical units. - Introduced Minimum Relative Edge Length preference to control effective spacing based on the bounding box of the cutout content. - Updated tests to validate the new edge length functionality and ensure consistent behavior across different resolutions and canvas sizes. - Removed deprecated parameters and adjusted related properties to align with the new edge length implementation. - Enhanced geometry sampling logic to utilize effective world spacing for both standard and fine outline modes.
354 lines
12 KiB
Python
354 lines
12 KiB
Python
import importlib
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
from tests.support.blender 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_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_model_preferences_keep_valid_choices_and_reset_unknown_keys(self):
|
|
module = self.anyimage.preferences
|
|
stored = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
|
for attribute, reader, valid, default in (
|
|
("geometry_model", module.configured_geometry_model, "MOGE3_VITL", "MOGE2_VITS_NORMAL"),
|
|
("background_model", module.configured_background_model, "BEN2_BASE", "BIREFNET_LITE"),
|
|
("upscale_model", module.configured_upscale_model, "REALESRGAN_X4PLUS", "REALESRGAN_GENERAL_WDN_X4V3"),
|
|
):
|
|
setattr(stored, attribute, valid)
|
|
self.assertEqual(reader(), valid)
|
|
setattr(stored, attribute, "UNKNOWN")
|
|
self.assertEqual(reader(), default)
|
|
self.assertEqual(getattr(stored, attribute), default)
|
|
self.assertEqual({item[0]: item[-1] for item in module.BACKGROUND_MODEL_ITEMS}["BEN2_BASE"], 0)
|
|
self.assertEqual({item[0]: item[-1] for item in module.UPSCALE_MODEL_ITEMS}["REALESRGAN_X4PLUS"], 2)
|
|
self.assertEqual({item[0]: item[-1] for item in self.anyimage.properties.geometry_model_enum_items(None, None)}["MOGE3_VITL"], 3)
|
|
|
|
|
|
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_relative_edge_length_defaults_and_clamps(self):
|
|
module = self.anyimage.preferences
|
|
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
|
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.01)
|
|
preferences.min_cutout_relative_edge_length = 2
|
|
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.02)
|
|
preferences.min_cutout_relative_edge_length = 200
|
|
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 1)
|
|
preferences.min_cutout_relative_edge_length = 0
|
|
self.assertAlmostEqual(module.configured_min_cutout_relative_edge_length(), 0.00001)
|
|
|
|
|
|
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 = []
|
|
|
|
class Layout:
|
|
enabled = True
|
|
|
|
def prop(self, _owner, name, **_options):
|
|
pass
|
|
|
|
def separator(self, **_options):
|
|
return None
|
|
|
|
def label(self, **_options):
|
|
pass
|
|
|
|
def row(self, **_options):
|
|
return self
|
|
|
|
def split(self, **_options):
|
|
return self
|
|
|
|
def column(self, **_options):
|
|
return self
|
|
|
|
def box(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 = "HAT_GAN_X4_SHARPER"
|
|
catalog = tuple(
|
|
{
|
|
"key": key,
|
|
"label": key,
|
|
"family": family,
|
|
"enabled": True,
|
|
"ready": False,
|
|
}
|
|
for key, family in (
|
|
("MOGE2_VITS_NORMAL", "moge"),
|
|
("BEN2_BASE", "ben2"),
|
|
("REALESRGAN_GENERAL_WDN_X4V3", "upscale"),
|
|
("HAT_GAN_X4_SHARPER", "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", "HAT_GAN_X4_SHARPER"],
|
|
)
|
|
|
|
|
|
def test_preferences_hide_ai_until_server_is_installed(self):
|
|
preference_module = importlib.import_module("anyimage.preferences")
|
|
operators = []
|
|
|
|
class Layout:
|
|
enabled = True
|
|
|
|
def box(self):
|
|
return self
|
|
|
|
def label(self, **options):
|
|
pass
|
|
|
|
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(
|
|
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.plane_type = "DEPTH"
|
|
operator.input_path = __file__
|
|
operator.analysis_input_path = __file__
|
|
operator.source_object_name = "Source"
|
|
operator.mesh_detail = 0
|
|
operator.model = self.anyimage.preferences.DEFAULT_GEOMETRY_MODEL_KEY
|
|
operator.resolution_level = 5
|
|
operator.max_input_size = 1536
|
|
context = SimpleNamespace(
|
|
scene=SimpleNamespace(anyimage_settings=SimpleNamespace())
|
|
)
|
|
with (
|
|
patch.object(self.convert_to_plane, "require_environment"),
|
|
patch.object(self.convert_to_plane, "require_model"),
|
|
patch.object(self.convert_to_plane, "require_input_path", return_value=Path("input.png")),
|
|
patch.object(self.convert_to_plane, "production_device", return_value="CPU"),
|
|
):
|
|
parameters = operator.request(context)
|
|
|
|
self.assertEqual(parameters["max_input_size"], 1536)
|
|
|
|
|
|
def test_upscale_execute_reads_model_and_size_from_preferences(self):
|
|
source_image = SimpleNamespace(
|
|
name="Original",
|
|
source="FILE",
|
|
)
|
|
source_object = SimpleNamespace(
|
|
name="Source",
|
|
type="EMPTY",
|
|
empty_display_type="IMAGE",
|
|
data=source_image,
|
|
image_user=None,
|
|
)
|
|
context = SimpleNamespace(
|
|
object=source_object,
|
|
scene=SimpleNamespace(anyimage_settings=SimpleNamespace()),
|
|
)
|
|
operator = self.upscale.UpscaleImage()
|
|
with (
|
|
patch.object(
|
|
self.upscale,
|
|
"configured_upscale_model",
|
|
return_value="HAT_GAN_X4_SHARPER",
|
|
),
|
|
patch.object(
|
|
self.upscale,
|
|
"configured_max_ai_input_size",
|
|
return_value=1024,
|
|
),
|
|
patch.object(
|
|
self.upscale.ImageEditTarget,
|
|
"prepare",
|
|
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, "HAT_GAN_X4_SHARPER")
|
|
self.assertEqual(operator.max_input_size, 1024)
|