feat(selection): separate selection geometry from image content processing

- Introduced a new design for selection rasterization that decouples the selection geometry from the image content, ensuring that the SelectionMask is independent of the source image's alpha and color.
- Updated the `rasterize_selection_path` function to only accept selection paths and image dimensions, removing parameters related to source alpha and alpha thresholds.
- Implemented a new approach for handling selection paths using non-zero winding fill, ensuring overlapping areas are preserved.
- Enhanced performance by utilizing a scanline algorithm for rasterization, significantly reducing processing time for high-resolution images.
- Updated internal documentation and added comprehensive tests to validate the new behavior and performance improvements.
- Ensured backward compatibility by maintaining existing interfaces where necessary while introducing breaking changes in the selection processing logic.
This commit is contained in:
2026-09-01 22:36:20 +08:00
parent 2ff7b04f56
commit 7f6e47432e
54 changed files with 2831 additions and 634 deletions

View File

@@ -1,9 +1,8 @@
import json
import sys
from pathlib import Path
from types import MethodType, SimpleNamespace
import bpy
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
@@ -83,6 +82,15 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
context = cutout_context(window_manager, mesh_detail="MEDIUM", generate_normal=True)
source = cutout_source(size=(4096, 2048))
monkeypatch.setattr(interaction, "ai_ready", lambda: True)
serialized = []
original_to_json = SelectionPath.to_json
def record_to_json(selection_path):
value = original_to_json(selection_path)
serialized.append(value)
return value
monkeypatch.setattr(SelectionPath, "to_json", record_to_json)
interaction.open_shape_pie(
context,
@@ -103,6 +111,12 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
]
assert all(call[2].refine_selection for call in window_manager.pie.calls)
assert all(call[2].generate_normal for call in window_manager.pie.calls)
assert len(serialized) == 1
assert set(json.loads(serialized[0])) == {"points", "invert"}
assert all(
call[2].selection_path_json == serialized[0]
for call in window_manager.pie.calls
)
assert all(
SelectionPath.from_json(call[2].selection_path_json).invert
for call in window_manager.pie.calls
@@ -132,12 +146,7 @@ def test_cutout_mesh_detail_maps_to_denser_sampling():
def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypatch):
bpy.ops.wm.read_factory_settings(use_empty=True)
image = bpy.data.images.new("Source", width=320, height=180)
source = bpy.data.objects.new("Source", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
source = cutout_source(size=(65536, 32768))
window_manager = WindowManager()
context = cutout_context(window_manager)
@@ -145,15 +154,32 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
cutout_main, "resolve_image_edit_click", lambda *_args: (source, True)
)
monkeypatch.setattr(cutout_main, "video_total_frames", lambda _image: 1)
monkeypatch.setattr(cutout_main, "require_image_empty", lambda _name: source)
monkeypatch.setattr(cutout_main, "apply_selection_settings", lambda *_args: None)
monkeypatch.setattr(cutout_main, "validate_cutout_selection", lambda *_args: None)
monkeypatch.setattr(cutout_main, "serialize_matrix", lambda _matrix: "matrix")
monkeypatch.setattr(
cutout_main,
"image_rgba",
lambda *_args: (_ for _ in ()).throw(AssertionError("unexpected pixel read")),
)
monkeypatch.setattr(
cutout_main,
"rasterize_selection_path",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("unexpected Selection rasterization")
),
)
operator = SimpleNamespace(
full_image=True,
refine_selection=False,
report=lambda *_args: None,
)
operator.execute = MethodType(cutout_main.SelectCutoutSelection.execute, operator)
source.matrix_world = object()
operator._submit_selection_path = MethodType(
cutout_main.SelectCutoutSelection._submit_selection_path,
operator,
)
result = cutout_main.SelectCutoutSelection.invoke(
operator, context, SimpleNamespace(value="DOUBLE_CLICK")
)
@@ -161,8 +187,13 @@ def test_cutout_reuses_image_lasso_gesture_and_double_click_selects_all(monkeypa
assert issubclass(cutout_main.SelectCutoutSelection, ImageSelectionGesture)
assert result == {"FINISHED"}
selection_path = SelectionPath.from_json(window_manager.pie.calls[0][2].selection_path_json)
assert selection_path.points == ((0, 0), (320, 0), (320, 180), (0, 180))
assert window_manager.pie.calls[0][2].mesh_resolution == 10
assert selection_path.points == (
(0, 0),
(65536, 0),
(65536, 32768),
(0, 32768),
)
assert window_manager.pie.calls[0][2].mesh_resolution == 1024
assert not hasattr(cutout_main, "pending_selection")