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

@@ -326,12 +326,10 @@ def triangulated_polygon_vertices(vertices):
return _triangulated_polygon_vertices_python(vertices)
def scanline_fill_triangles(vertices, fill_rule="EVEN_ODD", step=2.0):
def scanline_fill_triangles(vertices, step=2.0):
vertices = tuple(tuple(float(value) for value in vertex) for vertex in vertices)
if len(vertices) < 3:
return ()
if fill_rule not in {"EVEN_ODD", "NONZERO"}:
raise ValueError(f"Unknown polygon fill rule: {fill_rule}")
minimum_y = min(vertex[1] for vertex in vertices)
maximum_y = max(vertex[1] for vertex in vertices)
band_bottom = minimum_y
@@ -358,17 +356,11 @@ def scanline_fill_triangles(vertices, fill_rule="EVEN_ODD", step=2.0):
while index < len(events):
x = events[index][0]
delta = 0
count = 0
while index < len(events) and abs(events[index][0] - x) <= 1e-8:
delta += events[index][1]
count += 1
index += 1
was_inside = winding != 0
if fill_rule == "EVEN_ODD":
if count % 2:
winding = 0 if was_inside else 1
else:
winding += delta
winding += delta
is_inside = winding != 0
if not was_inside and is_inside:
interval_start = x
@@ -454,12 +446,12 @@ def polygon_self_intersects(vertices, cell_size=32.0):
return False
def preview_fill_triangles(vertices, fill_rule="EVEN_ODD"):
def preview_fill_triangles(vertices):
if not polygon_self_intersects(vertices):
triangles = triangulated_polygon_vertices(vertices)
if triangles:
return triangles
return scanline_fill_triangles(vertices, fill_rule)
return scanline_fill_triangles(vertices)
def draw_polygon_fill(
@@ -609,7 +601,6 @@ def image_line_half_plane_polygon(line, image_size):
class ImageSelectionGesture:
active_tool_id = None
preview_fill_rule = "EVEN_ODD"
_path = None
_cursor = None
_handle = None
@@ -658,9 +649,7 @@ class ImageSelectionGesture:
if len(polygon) >= 3:
if polygon != self._fill_polygon:
self._fill_polygon = polygon
self._fill_triangles = preview_fill_triangles(
polygon, self.preview_fill_rule
)
self._fill_triangles = preview_fill_triangles(polygon)
draw_polygon_fill(
polygon,
self._fill_triangles,
@@ -725,6 +714,9 @@ class ImageSelectionGesture:
def cancel(self, context):
self._finish(context)
def _submit_selection_path(self, _context, _selection_path):
raise NotImplementedError
def invoke(self, context, event):
source_object, should_edit = resolve_image_edit_click(
context, event, self.report
@@ -740,7 +732,6 @@ class ImageSelectionGesture:
self.invert = False
self.source_object_name = source_object.name
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
self.selection_path_json = ""
point = (float(event.mouse_region_x), float(event.mouse_region_y))
self._path = [point]
self._cursor = point
@@ -787,13 +778,12 @@ class ImageSelectionGesture:
image_empty_bounds(source_object),
tuple(source_object.data.size),
)
self.selection_path_json = SelectionPath(
selection_path = SelectionPath(
points=selection_path,
invert=self.invert,
fill_rule=self.preview_fill_rule,
).to_json()
)
self._finish(context)
return self.execute(context)
return self._submit_selection_path(context, selection_path)
def modal(self, context, event):
active_tool = active_view3d_tool_id(context)