feat: Enhance AI integration for Cutout, Image Box, and Image Lasso tools with unified settings and improved shape selection

This commit is contained in:
2026-08-31 16:10:08 +08:00
parent e7aeeb21d4
commit 14d63001d2
15 changed files with 473 additions and 89 deletions

View File

@@ -161,35 +161,36 @@ def apply_region_settings(operator, context):
)
def draw_ai_setup(layout):
status = ai_status()
if not status["ready"]:
layout.operator(
"anyimage.setup_ai_environment",
text=ai_setup_label(status),
icon="IMPORT",
)
return status
def draw_ai_property(layout, owner, name, status):
row = layout.row()
row.enabled = status["ready"]
row.prop(owner, name)
def draw_region_operator_settings(layout, operator):
if operator.gesture not in {"BOX", "LASSO"}:
layout.prop(operator, "invert")
return
status = ai_status()
row = layout.row()
row.enabled = status["ready"]
row.prop(operator, "refine_region")
status = draw_ai_setup(layout)
draw_ai_property(layout, operator, "refine_region", status)
layout.prop(operator, "invert")
if not status["ready"]:
layout.operator(
"anyimage.setup_ai_environment",
text=ai_setup_label(status),
icon="IMPORT",
)
def draw_region_settings(context, layout, setting_name):
settings = context.scene.anyimage_settings
status = ai_status()
row = layout.row()
row.enabled = status["ready"]
row.prop(settings, setting_name)
if not status["ready"]:
layout.operator(
"anyimage.setup_ai_environment",
text=ai_setup_label(status),
icon="IMPORT",
)
status = draw_ai_setup(layout)
draw_ai_property(layout, settings, setting_name, status)
def dashed_line_segments(vertices, dash_length=4.0, gap_length=4.0):

View File

@@ -31,7 +31,11 @@ def set_modifier_input(modifier, identifier, value):
inputs = _modifier_input_slots(modifier)
if inputs is not None:
# Blender 5.2+ exposes inputs as runtime RNA keyed by socket identifier.
getattr(inputs, identifier).value = value
input_slot = getattr(inputs, identifier)
value_property = input_slot.bl_rna.properties["value"]
if value_property.type == "ENUM" and type(value) is int:
value = value_property.enum_items[value].identifier
input_slot.value = value
else:
# Blender 4.x exposes inputs as modifier ID properties.
modifier[identifier] = value

View File

@@ -75,7 +75,7 @@ def acquire_packed_image(image_data, suffix):
with os.fdopen(file_descriptor, "wb") as temporary_file:
temporary_file.write(image_data)
image = bpy.data.images.load(temporary_path, check_existing=False)
image.name = "Clipboard Image"
image.name = "Clipboard"
image.pack()
image[CLIPBOARD_HASH_PROPERTY] = image_hash
return image, False

View File

@@ -4,11 +4,19 @@ from ...preferences import (
MAX_CUTOUT_MESH_RESOLUTION,
MIN_CUTOUT_MESH_RESOLUTION,
)
from ...properties import ai_ready
from ...common.image_data import image_rgba
from ...common.image_region import rasterize_region_path
from .geometry import CUTOUT_ALPHA_THRESHOLD
from .shape import CUTOUT_SHAPE_LABELS
CUTOUT_SHAPE_BUTTONS = (
("SURFACE", "MESH_GRID"),
("BALLOON", "MESH_UVSPHERE"),
("DEPTH_BALLOON", "META_BALL"),
("DEPTH_SURFACE", "SURFACE_NCURVE"),
)
def validate_cutout_region(
source_object,
@@ -69,55 +77,28 @@ def open_shape_pie(
region_path_json = region_path.to_json()
settings = context.scene.anyimage_settings
point_spacing = settings.cutout_point_spacing
generate_normal = bool(getattr(settings, "cutout_generate_normal", False))
ready = ai_ready()
refine_region = ready and refine_region
generate_normal = ready and bool(
getattr(settings, "cutout_generate_normal", False)
)
mesh_resolution = cutout_mesh_resolution(source_object.data.size, point_spacing)
def draw(menu, _context):
pie = menu.layout.menu_pie()
_pie_button(
pie,
"SURFACE",
CUTOUT_SHAPE_LABELS["SURFACE"],
"MESH_GRID",
source_name,
region_path_json,
refine_region,
generate_normal,
mesh_resolution,
)
_pie_button(
pie,
"BALLOON",
CUTOUT_SHAPE_LABELS["BALLOON"],
"MESH_UVSPHERE",
source_name,
region_path_json,
refine_region,
generate_normal,
mesh_resolution,
)
_pie_button(
pie,
"DEPTH_BALLOON",
CUTOUT_SHAPE_LABELS["DEPTH_BALLOON"],
"META_BALL",
source_name,
region_path_json,
refine_region,
generate_normal,
mesh_resolution,
)
_pie_button(
pie,
"DEPTH_SURFACE",
CUTOUT_SHAPE_LABELS["DEPTH_SURFACE"],
"SURFACE_NCURVE",
source_name,
region_path_json,
refine_region,
generate_normal,
mesh_resolution,
)
buttons = CUTOUT_SHAPE_BUTTONS if ready else CUTOUT_SHAPE_BUTTONS[:2]
for shape, icon in buttons:
_pie_button(
pie,
shape,
CUTOUT_SHAPE_LABELS[shape],
icon,
source_name,
region_path_json,
refine_region,
generate_normal,
mesh_resolution,
)
context.window_manager.popup_menu_pie(
event,

View File

@@ -43,7 +43,8 @@ from ...common.depth_data import load_depth_metadata, load_depth_result_image
from ...common.image_interaction import (
ImageRegionGesture,
apply_region_settings,
draw_region_settings,
draw_ai_property,
draw_ai_setup,
gesture_property,
report_image_edit_exception,
resolve_image_edit_click,
@@ -450,6 +451,7 @@ class CutoutTool(bpy.types.WorkSpaceTool):
def draw_settings(context, layout, _tool):
settings = context.scene.anyimage_settings
draw_region_settings(context, layout, "cutout_refine_region")
status = draw_ai_setup(layout)
draw_ai_property(layout, settings, "cutout_refine_region", status)
layout.prop(settings, "cutout_point_spacing")
layout.prop(settings, "cutout_generate_normal")
draw_ai_property(layout, settings, "cutout_generate_normal", status)

View File

@@ -109,6 +109,19 @@ def preview_polygon(points):
return points[np.argsort(np.arctan2(points[:, 1] - center[1], points[:, 0] - center[0]))]
def interactive_preview_polygon(points, cursor):
"""Return the clicked points plus one distinct moving cursor point."""
vertices = list(points)
if (
cursor is not None
and vertices
and len(vertices) < 4
and math.dist(vertices[-1], cursor) >= 2.0
):
vertices.append(cursor)
return preview_polygon(vertices)
def clip_polygon_to_image(points, image_size):
"""Clip one polygon to the source image rectangle."""
polygon = [tuple(float(value) for value in point) for point in points]
@@ -283,7 +296,7 @@ def create_perspective_preview_image(source_image, pixels):
alpha=True,
)
try:
image.colorspace_settings.name = source_image.colorspace_settings.name
image.colorspace_settings.name = "sRGB"
image.alpha_mode = "STRAIGHT"
image.pixels.foreach_set(pixels)
image.update()
@@ -293,6 +306,12 @@ def create_perspective_preview_image(source_image, pixels):
return image
def preview_texture_draw_options(blender_version):
if tuple(blender_version) >= (5, 0, 0):
return {"is_scene_linear_with_rec709_srgb_target": True}
return {}
def homography_from_points(source, target):
"""Return a projective transform mapping four source points to target."""
import numpy as np
@@ -526,7 +545,13 @@ class EditImagePerspective(bpy.types.Operator):
texture = gpu.texture.from_image(self._preview_image)
gpu.state.blend_set("ALPHA")
try:
draw_texture_2d(texture, (left, bottom), width, height)
draw_texture_2d(
texture,
(left, bottom),
width,
height,
**preview_texture_draw_options(bpy.app.version),
)
finally:
gpu.state.blend_set("NONE")
draw_preview_frame(bounds)
@@ -542,10 +567,7 @@ class EditImagePerspective(bpy.types.Operator):
if self._phase == "ASPECT":
self._draw_preview()
return
vertices = list(self._points)
if self._cursor is not None and len(vertices) < 4:
vertices.append(self._cursor)
vertices = list(preview_polygon(vertices))
vertices = list(interactive_preview_polygon(self._points, self._cursor))
draw_polygon_fill(vertices)
if len(vertices) > 2:
vertices.append(vertices[0])