Add tests and implement boundary smoothing and cutout functionality

- Introduced tests for boundary smoothing in `test_boundary_smoothing.py`, verifying that smoothing reduces stair-step artifacts while preserving boundary integrity.
- Added tests for cutout boundary behavior in `test_cutout_boundary.py`, ensuring proper handling of boundary triangles and normal blending.
- Created `test_temporary_attributes.py` to check the cleanup of temporary attributes across depth modifier branches, ensuring no warnings are raised.
- Implemented point culling functionality in `test_validity.py`, confirming that invalid points are removed without affecting adjacent geometry.
- Developed boundary smoothing logic in `boundary_smoothing.py`, allowing for depth smoothing while maintaining edge integrity.
- Added cutout boundary management in `cutout_boundary.py`, including functions for removing boundary triangles and tapering thickness profiles.
- Implemented validity checks in `validity.py` to ensure proper deletion of invalid vertices and associated wire edges.
This commit is contained in:
2026-09-11 20:37:20 +08:00
parent e8129fe495
commit 31074bd511
85 changed files with 2501 additions and 1451 deletions

View File

@@ -631,6 +631,89 @@ 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(