feat(cutout): implement boundary padding for cutout textures
- Introduced a new `Boundary Padding` preference in the Cutout Tool with a default of 2 px and a configurable range of 1-4 px. - Updated the Cutout client to establish inward mapping from the final Mask, sampling in a fixed direction and processing independent material RGBA and Depth. - Enhanced the handling of RGBA and Depth during cutout operations, ensuring proper mapping and preservation of source data. - Added tests to validate the new boundary padding functionality and its integration with existing cutout processes. feat(geometry): remove overall mesh smoothing from geometry nodes - Removed the `Smooth` input from several geometry node groups to eliminate unnecessary overall mesh smoothing. - Retained `Smooth Weight` for depth nodes while removing it from Cutout and Relief Plane nodes. - Ensured that boundary, normal, and fill smoothing behaviors remain intact and consistent. - Updated tests and documentation to reflect the changes in node interfaces and behaviors. feat(normals): scale tangent normals with depth - Reused the existing `o_normal_reduction` attribute to adjust Tangent Normal strength based on the geometry's `Depth Scale`. - Ensured that Tangent Normal strength is consistent with the geometry depth, preserving full strength for materials that do not provide the attribute. - Updated tests to verify the correct behavior of normal strength adjustments in both Tangent and Object Space.
This commit is contained in:
@@ -1,89 +1,23 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib
|
||||
from contextlib import nullcontext
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from tests.support.blender import mock_bpy_imports
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class ClipboardImageTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.previous_addon_modules = {
|
||||
name: module for name, module in sys.modules.items()
|
||||
if name == "anyimage" or name.startswith("anyimage.")
|
||||
}
|
||||
cls.temporary_directory = tempfile.TemporaryDirectory()
|
||||
cls.remove_anyimage_modules()
|
||||
fake_bpy = ModuleType("bpy")
|
||||
fake_bpy.app = SimpleNamespace(version=(5, 1, 0))
|
||||
fake_bpy.props = SimpleNamespace(
|
||||
BoolProperty=lambda **options: options,
|
||||
CollectionProperty=lambda **options: options,
|
||||
EnumProperty=lambda **options: options,
|
||||
FloatProperty=lambda **options: options,
|
||||
FloatVectorProperty=lambda **options: options,
|
||||
IntProperty=lambda **options: options,
|
||||
PointerProperty=lambda **options: options,
|
||||
StringProperty=lambda **options: options,
|
||||
)
|
||||
fake_bpy.types = SimpleNamespace(
|
||||
AddonPreferences=object,
|
||||
GizmoGroup=object,
|
||||
Image=object,
|
||||
Menu=object,
|
||||
Operator=object,
|
||||
Panel=object,
|
||||
PropertyGroup=object,
|
||||
Scene=object,
|
||||
TOPBAR_MT_file_defaults=SimpleNamespace(),
|
||||
)
|
||||
fake_bpy.utils = SimpleNamespace()
|
||||
fake_bpy.context = SimpleNamespace(
|
||||
window_manager=SimpleNamespace(
|
||||
keyconfigs=SimpleNamespace(addon=None),
|
||||
)
|
||||
)
|
||||
fake_bpy.data = SimpleNamespace()
|
||||
cls.bpy_import_patch = mock_bpy_imports(fake_bpy)
|
||||
cls.bpy_import_patch.start()
|
||||
cls.addClassCleanup(cls.bpy_import_patch.stop)
|
||||
cls.fake_bpy = fake_bpy
|
||||
|
||||
cls.addon = ModuleType("anyimage")
|
||||
cls.addon.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
|
||||
sys.modules["anyimage"] = cls.addon
|
||||
operators = ModuleType("anyimage.operators")
|
||||
operators.__path__ = [str(PROJECT_ROOT / "src" / "anyimage" / "operators")]
|
||||
sys.modules["anyimage.operators"] = operators
|
||||
cls.addon.clipboard_image = importlib.import_module(
|
||||
class ClipboardImageTest(BlenderTestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.clipboard_image = importlib.import_module(
|
||||
"anyimage.operators.clipboard_image.operators"
|
||||
)
|
||||
cls.actions = importlib.import_module("anyimage.operators.clipboard_image.actions")
|
||||
cls.clipboard = importlib.import_module("anyimage.operators.clipboard_image.clipboard")
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.remove_anyimage_modules()
|
||||
cls.temporary_directory.cleanup()
|
||||
sys.modules.update(cls.previous_addon_modules)
|
||||
|
||||
def test_operators_use_anyimage_prefix_and_plain_properties(self):
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
self.assertEqual(
|
||||
clipboard_image.PasteClipboardImage.bl_idname,
|
||||
"anyimage.paste_clipboard_image",
|
||||
self.actions = importlib.import_module(
|
||||
"anyimage.operators.clipboard_image.actions"
|
||||
)
|
||||
self.assertEqual(
|
||||
clipboard_image.TrackNativeCopy.bl_idname,
|
||||
"anyimage.track_native_copy",
|
||||
self.clipboard = importlib.import_module(
|
||||
"anyimage.operators.clipboard_image.clipboard"
|
||||
)
|
||||
for name in clipboard_image.PasteClipboardImage.__annotations__:
|
||||
self.assertFalse(name.startswith("anyimage_"), name)
|
||||
|
||||
def test_registers_and_removes_clipboard_shortcuts(self):
|
||||
class KeyMapItems:
|
||||
@@ -122,11 +56,12 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
addon=SimpleNamespace(keymaps=keymaps),
|
||||
),
|
||||
)
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
clipboard_image = self.clipboard_image
|
||||
with patch.object(
|
||||
self.fake_bpy.context,
|
||||
"window_manager",
|
||||
window_manager,
|
||||
create=True,
|
||||
):
|
||||
clipboard_image.register_keymaps()
|
||||
items = [
|
||||
@@ -204,36 +139,29 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(self.actions.target_for_context(context))
|
||||
|
||||
def test_world_shader_uses_environment_texture(self):
|
||||
world_space = SimpleNamespace(
|
||||
tree_type="ShaderNodeTree",
|
||||
shader_type="WORLD",
|
||||
)
|
||||
material_space = SimpleNamespace(
|
||||
tree_type="ShaderNodeTree",
|
||||
shader_type="OBJECT",
|
||||
)
|
||||
|
||||
def test_node_type_follows_the_world_space(self):
|
||||
self.assertEqual(
|
||||
self.actions._node_type_for_space(world_space),
|
||||
self.actions._node_type_for_space(
|
||||
SimpleNamespace(tree_type="ShaderNodeTree", shader_type="WORLD")
|
||||
),
|
||||
"ShaderNodeTexEnvironment",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.actions._node_type_for_space(material_space),
|
||||
self.actions._node_type_for_space(
|
||||
SimpleNamespace(tree_type="ShaderNodeTree", shader_type="OBJECT")
|
||||
),
|
||||
"ShaderNodeTexImage",
|
||||
)
|
||||
|
||||
def test_pinned_world_tree_uses_environment_texture(self):
|
||||
world_space = SimpleNamespace(
|
||||
tree_type="ShaderNodeTree",
|
||||
shader_type="OBJECT",
|
||||
id=SimpleNamespace(
|
||||
bl_rna=SimpleNamespace(identifier="World"),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.actions._node_type_for_space(world_space),
|
||||
self.actions._node_type_for_space(
|
||||
SimpleNamespace(
|
||||
tree_type="ShaderNodeTree",
|
||||
shader_type="OBJECT",
|
||||
id=SimpleNamespace(
|
||||
bl_rna=SimpleNamespace(identifier="World"),
|
||||
),
|
||||
)
|
||||
),
|
||||
"ShaderNodeTexEnvironment",
|
||||
)
|
||||
|
||||
@@ -254,67 +182,15 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(location, (85.0, 55.0))
|
||||
view2d.region_to_view.assert_called_once_with(80, 60)
|
||||
|
||||
def test_node_location_without_event_uses_node_cursor(self):
|
||||
context = SimpleNamespace(
|
||||
cursor_context = SimpleNamespace(
|
||||
space_data=SimpleNamespace(cursor_location=(12.0, -8.0)),
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.actions._node_location(context, None),
|
||||
self.actions._node_location(cursor_context, None),
|
||||
(12.0, -8.0),
|
||||
)
|
||||
|
||||
def test_view3d_location_projects_mouse_at_cursor_depth(self):
|
||||
projected_location = (4.0, 5.0, 6.0)
|
||||
project = Mock(return_value=projected_location)
|
||||
bpy_extras = ModuleType("bpy_extras")
|
||||
view3d_utils = ModuleType("bpy_extras.view3d_utils")
|
||||
view3d_utils.region_2d_to_location_3d = project
|
||||
bpy_extras.view3d_utils = view3d_utils
|
||||
cursor_location = Mock()
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(
|
||||
cursor=SimpleNamespace(location=cursor_location),
|
||||
),
|
||||
region=object(),
|
||||
region_data=object(),
|
||||
)
|
||||
event = SimpleNamespace(mouse_region_x=120, mouse_region_y=75)
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"bpy_extras": bpy_extras,
|
||||
"bpy_extras.view3d_utils": view3d_utils,
|
||||
},
|
||||
):
|
||||
result = self.actions._view3d_location(context, event)
|
||||
|
||||
self.assertIs(result, projected_location)
|
||||
project.assert_called_once_with(
|
||||
context.region,
|
||||
context.region_data,
|
||||
(120, 75),
|
||||
cursor_location,
|
||||
)
|
||||
|
||||
def test_view3d_location_without_event_copies_cursor_location(self):
|
||||
copied_location = object()
|
||||
cursor_location = SimpleNamespace(
|
||||
copy=Mock(return_value=copied_location),
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
scene=SimpleNamespace(
|
||||
cursor=SimpleNamespace(location=cursor_location),
|
||||
),
|
||||
)
|
||||
|
||||
result = self.actions._view3d_location(context)
|
||||
|
||||
self.assertIs(result, copied_location)
|
||||
cursor_location.copy.assert_called_once_with()
|
||||
|
||||
def test_preserves_image_aspect_ratio_for_plane(self):
|
||||
self.assertEqual(
|
||||
self.actions._plane_half_dimensions(1920, 1080),
|
||||
@@ -325,28 +201,8 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
(2 / 3, 1.0),
|
||||
)
|
||||
|
||||
def test_plane_import_defaults_to_reference_image(self):
|
||||
options = (
|
||||
self.addon.clipboard_image.PasteClipboardImage
|
||||
.__annotations__["import_as"]
|
||||
)
|
||||
|
||||
self.assertEqual(options["default"], "REFERENCE")
|
||||
self.assertEqual(
|
||||
[item[0] for item in options["items"]],
|
||||
["PLANE", "REFERENCE"],
|
||||
)
|
||||
|
||||
def test_plane_shadeless_is_disabled_by_default(self):
|
||||
options = (
|
||||
self.addon.clipboard_image.PasteClipboardImage
|
||||
.__annotations__["shadeless"]
|
||||
)
|
||||
|
||||
self.assertFalse(options["default"])
|
||||
|
||||
def test_native_copy_takes_priority_while_clipboard_is_unchanged(self):
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
def test_native_copy_defers_paste_until_the_clipboard_changes(self):
|
||||
clipboard_image = self.clipboard_image
|
||||
original_token = clipboard_image._native_copy_token
|
||||
try:
|
||||
clipboard_image._native_copy_token = 41
|
||||
@@ -356,27 +212,12 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
return_value=41,
|
||||
):
|
||||
self.assertTrue(clipboard_image._should_defer_to_native_paste())
|
||||
finally:
|
||||
clipboard_image._native_copy_token = original_token
|
||||
|
||||
def test_new_system_clipboard_content_restores_image_paste(self):
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
original_token = clipboard_image._native_copy_token
|
||||
try:
|
||||
clipboard_image._native_copy_token = 41
|
||||
with patch.object(
|
||||
clipboard_image,
|
||||
"clipboard_change_token",
|
||||
return_value=42,
|
||||
):
|
||||
self.assertFalse(clipboard_image._should_defer_to_native_paste())
|
||||
finally:
|
||||
clipboard_image._native_copy_token = original_token
|
||||
|
||||
def test_native_copy_tracker_passes_through(self):
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
original_token = clipboard_image._native_copy_token
|
||||
try:
|
||||
with patch.object(
|
||||
clipboard_image,
|
||||
"clipboard_change_token",
|
||||
@@ -388,52 +229,6 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
finally:
|
||||
clipboard_image._native_copy_token = original_token
|
||||
|
||||
def test_plane_paste_uses_shared_material_for_both_shading_modes(self):
|
||||
from anyimage.operators.convert_to_plane import image_plane
|
||||
|
||||
image = SimpleNamespace(name="Clipboard", size=(16, 8))
|
||||
context = SimpleNamespace(mode="OBJECT", scene=object())
|
||||
for shadeless in (False, True):
|
||||
with (
|
||||
patch.object(self.actions, "create_image_material", return_value="Material") as create_material,
|
||||
patch.object(self.actions, "material_color_image", return_value=nullcontext("Color")),
|
||||
patch.object(image_plane, "create_image_plane_object") as create_plane,
|
||||
patch.object(self.actions, "_place_in_view"),
|
||||
patch.object(self.actions, "_select_only"),
|
||||
):
|
||||
self.actions.add_image_plane(context, image, shadeless=shadeless)
|
||||
create_material.assert_called_once_with(
|
||||
image, "Color", shadeless=shadeless, scene=context.scene,
|
||||
)
|
||||
self.assertEqual(create_plane.call_args.args[3], "Material")
|
||||
|
||||
def test_reference_import_routes_to_image_empty(self):
|
||||
context = self.make_context("VIEW_3D")
|
||||
image = object()
|
||||
expected_reference = object()
|
||||
expected_location = (1.0, 2.0, 3.0)
|
||||
original_add_reference_image = self.actions.add_reference_image
|
||||
self.actions.add_reference_image = (
|
||||
lambda received_context, received_image, location=None: expected_reference
|
||||
if (
|
||||
received_context is context
|
||||
and received_image is image
|
||||
and location == expected_location
|
||||
)
|
||||
else None
|
||||
)
|
||||
try:
|
||||
result = self.actions.paste_image(
|
||||
context,
|
||||
image,
|
||||
location=expected_location,
|
||||
import_as="REFERENCE",
|
||||
)
|
||||
finally:
|
||||
self.actions.add_reference_image = original_add_reference_image
|
||||
|
||||
self.assertIs(result, expected_reference)
|
||||
|
||||
def test_reference_image_uses_drag_and_drop_size(self):
|
||||
reference = SimpleNamespace(select_set=Mock())
|
||||
objects = SimpleNamespace(new=Mock(return_value=reference))
|
||||
@@ -456,32 +251,8 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
self.assertIs(result, reference)
|
||||
self.assertEqual(reference.empty_display_size, 5.0)
|
||||
|
||||
def test_only_import_as_and_shadeless_are_drawn_for_plane_paste(self):
|
||||
operator = self.addon.clipboard_image.PasteClipboardImage()
|
||||
drawn_properties = []
|
||||
operator.layout = SimpleNamespace(
|
||||
prop=lambda owner, name: drawn_properties.append((owner, name)),
|
||||
)
|
||||
|
||||
operator.paste_target = "PLANE"
|
||||
operator.import_as = "PLANE"
|
||||
operator.draw(None)
|
||||
operator.import_as = "REFERENCE"
|
||||
operator.draw(None)
|
||||
operator.paste_target = "NODE"
|
||||
operator.draw(None)
|
||||
|
||||
self.assertEqual(
|
||||
drawn_properties,
|
||||
[
|
||||
(operator, "import_as"),
|
||||
(operator, "shadeless"),
|
||||
(operator, "import_as"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_redo_reuses_captured_node_location(self):
|
||||
operator = self.addon.clipboard_image.PasteClipboardImage()
|
||||
operator = self.clipboard_image.PasteClipboardImage()
|
||||
operator.paste_target = "NODE"
|
||||
operator.location = (10.0, 20.0, 0.0)
|
||||
operator.location_set = True
|
||||
@@ -494,7 +265,7 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
)
|
||||
image = SimpleNamespace(name="Clipboard Image")
|
||||
|
||||
clipboard_image = self.addon.clipboard_image
|
||||
clipboard_image = self.clipboard_image
|
||||
with (
|
||||
patch.object(
|
||||
clipboard_image,
|
||||
@@ -626,161 +397,6 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
self.assertEqual(brush.texture_slot.map_mode, "STENCIL")
|
||||
self.assertEqual(brush.stencil_dimension, (256.0, 144.0))
|
||||
|
||||
def test_brush_preview_updates_local_copy_only_in_texture_paint(self):
|
||||
for mode in ("PAINT_TEXTURE", "SCULPT", "PAINT_VERTEX"):
|
||||
with self.subTest(mode=mode):
|
||||
brush = SimpleNamespace(
|
||||
texture=None,
|
||||
texture_slot=SimpleNamespace(texture=None),
|
||||
stencil_dimension=(256.0, 256.0),
|
||||
update_tag=Mock(),
|
||||
)
|
||||
context = SimpleNamespace(mode=mode, brush=object())
|
||||
images = [SimpleNamespace(name=name, size=(80, 40)) for name in ("A", "B")]
|
||||
textures = FakeTextures()
|
||||
with (
|
||||
patch.object(self.actions.bpy, "data", SimpleNamespace(textures=textures)),
|
||||
patch.object(self.actions, "_ensure_editable_brush", return_value=brush),
|
||||
patch.object(self.actions, "_set_brush_preview") as preview,
|
||||
):
|
||||
for image in images:
|
||||
self.actions.set_tool_texture(context, image)
|
||||
self.assertIs(brush.texture.image, images[-1])
|
||||
self.assertEqual(preview.call_count, 2 if mode == "PAINT_TEXTURE" else 0)
|
||||
if mode == "PAINT_TEXTURE":
|
||||
self.assertEqual([call.args[1:] for call in preview.call_args_list],
|
||||
[(brush, image) for image in images])
|
||||
|
||||
def test_brush_preview_cleans_export_on_success_cancel_and_error(self):
|
||||
for outcome in ({"FINISHED"}, {"CANCELLED"}, RuntimeError("preview failed")):
|
||||
with self.subTest(outcome=outcome):
|
||||
brush, image = object(), object()
|
||||
context = SimpleNamespace(scene=object(), temp_override=Mock(return_value=nullcontext()))
|
||||
loader = Mock()
|
||||
if isinstance(outcome, Exception):
|
||||
loader.side_effect = outcome
|
||||
else:
|
||||
loader.return_value = outcome
|
||||
with (
|
||||
patch.object(self.actions, "save_packed_image", return_value="preview.png"),
|
||||
patch.object(self.actions, "cleanup_image_input") as cleanup,
|
||||
patch.object(self.actions.bpy, "ops", SimpleNamespace(
|
||||
ed=SimpleNamespace(lib_id_load_custom_preview=loader)), create=True),
|
||||
):
|
||||
expectation = nullcontext() if outcome == {"FINISHED"} else self.assertRaises(self.actions.BrushPreviewError)
|
||||
with expectation:
|
||||
self.actions._set_brush_preview(context, brush, image)
|
||||
cleanup.assert_called_once_with("preview.png", True)
|
||||
context.temp_override.assert_called_once_with(id=brush)
|
||||
loader.assert_called_once_with(filepath="preview.png")
|
||||
|
||||
def test_preview_failure_preserves_texture_and_redraws(self):
|
||||
textures = FakeTextures()
|
||||
brush = SimpleNamespace(
|
||||
texture=None, texture_slot=SimpleNamespace(texture=None),
|
||||
stencil_dimension=(256.0, 256.0), update_tag=Mock(),
|
||||
)
|
||||
context = SimpleNamespace(mode="PAINT_TEXTURE", brush=brush, area=SimpleNamespace(tag_redraw=Mock()))
|
||||
image = SimpleNamespace(name="Clipboard", size=(80, 40))
|
||||
with (
|
||||
patch.object(self.actions.bpy, "data", SimpleNamespace(textures=textures)),
|
||||
patch.object(self.actions, "_set_brush_preview", side_effect=self.actions.BrushPreviewError("preview failed")),
|
||||
self.assertRaises(self.actions.BrushPreviewError),
|
||||
):
|
||||
self.actions.set_tool_texture(context, image)
|
||||
self.assertEqual(len(textures), 1)
|
||||
self.assertIs(brush.texture.image, image)
|
||||
context.area.tag_redraw.assert_called_once()
|
||||
|
||||
def test_preview_warning_finishes_paste(self):
|
||||
module = self.addon.clipboard_image
|
||||
operator = module.PasteClipboardImage()
|
||||
operator.paste_target = "TOOL_TEXTURE"
|
||||
operator.location_set = False
|
||||
operator.import_as = "REFERENCE"
|
||||
operator.shadeless = False
|
||||
operator.report = Mock()
|
||||
with (
|
||||
patch.object(module, "_should_defer_to_native_paste", return_value=False),
|
||||
patch.object(module, "read_clipboard_image", return_value=(b"image", ".png")),
|
||||
patch.object(module, "acquire_packed_image", return_value=(object(), False)),
|
||||
patch.object(module, "paste_image", side_effect=self.actions.BrushPreviewError("preview failed")),
|
||||
):
|
||||
self.assertEqual(operator.execute(SimpleNamespace()), {"FINISHED"})
|
||||
operator.report.assert_called_once_with({"WARNING"}, "preview failed")
|
||||
|
||||
def test_localizes_read_only_active_brush(self):
|
||||
local_brush = SimpleNamespace(is_editable=True, library=None)
|
||||
linked_brush = SimpleNamespace(
|
||||
is_editable=False,
|
||||
library=None,
|
||||
make_local=lambda: local_brush,
|
||||
)
|
||||
|
||||
self.assertIs(
|
||||
self.actions._ensure_editable_brush(
|
||||
SimpleNamespace(),
|
||||
linked_brush,
|
||||
),
|
||||
local_brush,
|
||||
)
|
||||
|
||||
def test_copies_and_activates_runtime_library_brush(self):
|
||||
local_brush = SimpleNamespace(
|
||||
name="Paint Hard Clipboard",
|
||||
is_editable=True,
|
||||
library=None,
|
||||
)
|
||||
library_brush = SimpleNamespace(
|
||||
name="Paint Hard",
|
||||
is_editable=True,
|
||||
library=object(),
|
||||
)
|
||||
paint_settings = SimpleNamespace(brush=library_brush)
|
||||
context = SimpleNamespace(
|
||||
mode="SCULPT",
|
||||
tool_settings=SimpleNamespace(sculpt=paint_settings),
|
||||
)
|
||||
|
||||
original_ops = getattr(self.actions.bpy, "ops", None)
|
||||
|
||||
def save_brush_as(**options):
|
||||
self.assertEqual(
|
||||
options["asset_library_reference"],
|
||||
"LOCAL",
|
||||
)
|
||||
self.assertEqual(options["name"], "Paint Hard Clipboard")
|
||||
paint_settings.brush = local_brush
|
||||
return {"FINISHED"}
|
||||
|
||||
self.actions.bpy.ops = SimpleNamespace(
|
||||
brush=SimpleNamespace(asset_save_as=save_brush_as),
|
||||
)
|
||||
try:
|
||||
result = self.actions._ensure_editable_brush(
|
||||
context,
|
||||
library_brush,
|
||||
)
|
||||
finally:
|
||||
if original_ops is None:
|
||||
del self.actions.bpy.ops
|
||||
else:
|
||||
self.actions.bpy.ops = original_ops
|
||||
|
||||
self.assertIs(result, local_brush)
|
||||
|
||||
def test_active_brush_prefers_current_mode_settings(self):
|
||||
correct_brush = object()
|
||||
context = SimpleNamespace(
|
||||
brush=object(),
|
||||
mode="SCULPT",
|
||||
tool_settings=SimpleNamespace(
|
||||
sculpt=SimpleNamespace(brush=correct_brush),
|
||||
),
|
||||
)
|
||||
|
||||
self.assertIs(self.actions._active_brush(context), correct_brush)
|
||||
|
||||
def test_vertex_and_texture_paint_use_unbiased_white(self):
|
||||
for mode in ("PAINT_VERTEX", "PAINT_TEXTURE"):
|
||||
with self.subTest(mode=mode):
|
||||
@@ -848,103 +464,6 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
with self.assertRaises(self.clipboard.ClipboardImageError):
|
||||
self.clipboard.dib_to_bmp(b"\x28\x00\x00\x00")
|
||||
|
||||
def test_reads_macos_clipboard_image_as_png(self):
|
||||
def export_image(_osascript, output_file):
|
||||
output_file.write_bytes(b"png image")
|
||||
|
||||
with patch.object(
|
||||
self.clipboard.shutil,
|
||||
"which",
|
||||
return_value="/usr/bin/osascript",
|
||||
), patch.object(
|
||||
self.clipboard,
|
||||
"_export_macos_clipboard_image",
|
||||
side_effect=export_image,
|
||||
):
|
||||
image_data, suffix = self.clipboard._read_macos_clipboard_image()
|
||||
|
||||
self.assertEqual(image_data, b"png image")
|
||||
self.assertEqual(suffix, ".png")
|
||||
|
||||
def test_macos_clipboard_without_image_is_unavailable(self):
|
||||
with patch.object(
|
||||
self.clipboard.shutil,
|
||||
"which",
|
||||
return_value="/usr/bin/osascript",
|
||||
), patch.object(
|
||||
self.clipboard,
|
||||
"_export_macos_clipboard_image",
|
||||
):
|
||||
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
|
||||
self.clipboard._read_macos_clipboard_image()
|
||||
|
||||
def test_linux_clipboard_prefers_wayland_png(self):
|
||||
def find_command(command_name):
|
||||
if command_name == "wl-paste":
|
||||
return "/usr/bin/wl-paste"
|
||||
return None
|
||||
|
||||
with patch.object(
|
||||
self.clipboard.shutil,
|
||||
"which",
|
||||
side_effect=find_command,
|
||||
), patch.object(
|
||||
self.clipboard,
|
||||
"_run_image_command",
|
||||
return_value=b"png image",
|
||||
) as run_command:
|
||||
image_data, suffix = self.clipboard._read_linux_clipboard_image()
|
||||
|
||||
self.assertEqual(image_data, b"png image")
|
||||
self.assertEqual(suffix, ".png")
|
||||
run_command.assert_called_once_with(
|
||||
[
|
||||
"/usr/bin/wl-paste",
|
||||
"--no-newline",
|
||||
"--type",
|
||||
"image/png",
|
||||
]
|
||||
)
|
||||
|
||||
def test_linux_clipboard_falls_back_to_xclip(self):
|
||||
def find_command(command_name):
|
||||
if command_name == "xclip":
|
||||
return "/usr/bin/xclip"
|
||||
return None
|
||||
|
||||
with patch.object(
|
||||
self.clipboard.shutil,
|
||||
"which",
|
||||
side_effect=find_command,
|
||||
), patch.object(
|
||||
self.clipboard,
|
||||
"_run_image_command",
|
||||
return_value=b"png image",
|
||||
) as run_command:
|
||||
image_data, suffix = self.clipboard._read_linux_clipboard_image()
|
||||
|
||||
self.assertEqual(image_data, b"png image")
|
||||
self.assertEqual(suffix, ".png")
|
||||
run_command.assert_called_once_with(
|
||||
[
|
||||
"/usr/bin/xclip",
|
||||
"-selection",
|
||||
"clipboard",
|
||||
"-t",
|
||||
"image/png",
|
||||
"-o",
|
||||
]
|
||||
)
|
||||
|
||||
def test_linux_clipboard_requires_supported_command(self):
|
||||
with patch.object(
|
||||
self.clipboard.shutil,
|
||||
"which",
|
||||
return_value=None,
|
||||
):
|
||||
with self.assertRaises(self.clipboard.ClipboardImageUnavailable):
|
||||
self.clipboard._read_linux_clipboard_image()
|
||||
|
||||
@staticmethod
|
||||
def make_context(area_type, mode="OBJECT", **space_options):
|
||||
return SimpleNamespace(
|
||||
@@ -954,13 +473,6 @@ class ClipboardImageTest(unittest.TestCase):
|
||||
space_data=SimpleNamespace(**space_options),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def remove_anyimage_modules():
|
||||
for module_name in tuple(sys.modules):
|
||||
if module_name == "anyimage" or module_name.startswith("anyimage."):
|
||||
sys.modules.pop(module_name)
|
||||
|
||||
|
||||
class FakeImage:
|
||||
def __init__(self, property_name, image_hash, packed):
|
||||
self.properties = {property_name: image_hash}
|
||||
@@ -1009,7 +521,3 @@ class FakeTextures(list):
|
||||
|
||||
def remove(self, texture):
|
||||
super().remove(texture)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user