Files
AnyImage/src/anyimage/operators/cutout.py
nan 7d39fe8c5e Add cutout profile and teddy balloon height calculations
- Implemented cotangent Poisson field and Poisson balloon profile in cutout_profile.py.
- Added teddy balloon heights calculation and related functions in cutout_teddy.py.
- Introduced interaction utilities for image editing in interaction.py.
- Developed depth normalization and analysis functions in cutout_depth.py.
- Created a validation script for cutout assets in validate_cutout_asset.py.
2026-08-27 22:46:57 +08:00

257 lines
7.7 KiB
Python

import json
import bpy
from ..properties import (
refine_selection_property,
)
from .image_edit.interaction import hidden_string_property, serialize_matrix
from .image import is_image_empty, video_total_frames
from .image_edit.common import (
ImageEditWarning,
apply_image_mask_settings,
draw_image_mask_settings,
polygon_bounds,
report_image_edit_exception,
resolve_image_edit_click,
)
from .image_edit.mask import (
ImageMaskGesture,
gesture_property,
selected_image_rgba,
)
from .mesh import BALLOON, CUTOUT, DEPTH_BALLOON, DEPTH_SURFACE
CUTOUT_DETAIL_MIN = 4
CUTOUT_DETAIL_MAX = 1024
def cutout_mesh_detail(image_size, point_spacing):
longest_edge = max(int(value) for value in image_size)
detail = round(longest_edge / max(int(point_spacing), 1))
return max(CUTOUT_DETAIL_MIN, min(CUTOUT_DETAIL_MAX, detail))
def _pie_button(
pie,
shape,
label,
icon,
source_name,
selection_data,
refine_selection,
normal_analysis,
invert,
mesh_detail,
):
operator = pie.operator(
"anyimage.convert_cutout_shape",
text=label,
icon=icon,
)
operator.source_object_name = source_name
operator.selection_data = selection_data
operator.shape = shape
operator.refine_selection = refine_selection
operator.normal_analysis = normal_analysis
operator.invert = invert
operator.mesh_detail = mesh_detail
def _open_shape_pie(
context,
event,
source_object,
polygon,
refine_selection=False,
invert=False,
):
source_name = source_object.name
selection_data = json.dumps(polygon, separators=(",", ":"))
settings = context.scene.anyimage_settings
point_spacing = settings.cutout_point_spacing
normal_analysis = bool(getattr(settings, "cutout_normal_analysis", False))
mesh_detail = cutout_mesh_detail(source_object.data.size, point_spacing)
def draw(menu, _context):
pie = menu.layout.menu_pie()
_pie_button(
pie,
CUTOUT,
"Surface",
"MESH_GRID",
source_name,
selection_data,
refine_selection,
normal_analysis,
invert,
mesh_detail,
)
_pie_button(
pie,
BALLOON,
"Balloon",
"MESH_UVSPHERE",
source_name,
selection_data,
refine_selection,
normal_analysis,
invert,
mesh_detail,
)
_pie_button(
pie,
DEPTH_BALLOON,
"Depth Balloon",
"META_BALL",
source_name,
selection_data,
refine_selection,
normal_analysis,
invert,
mesh_detail,
)
_pie_button(
pie,
DEPTH_SURFACE,
"Depth Surface",
"SURFACE_NCURVE",
source_name,
selection_data,
refine_selection,
normal_analysis,
invert,
mesh_detail,
)
context.window_manager.popup_menu_pie(
event,
draw,
title="Cutout Shape",
icon="MESH_DATA",
)
class SelectCutoutRegion(ImageMaskGesture, bpy.types.Operator):
bl_idname = "anyimage.select_cutout_region"
bl_label = "Select Cutout Region"
bl_description = "Lasso an Image Empty region, then choose its mesh shape"
bl_options = {"INTERNAL"}
active_tool_id = "anyimage.cutout_lasso"
source_object_name: hidden_string_property()
source_matrix_data: hidden_string_property()
selection_data: hidden_string_property()
refine_selection: refine_selection_property()
invert: bpy.props.BoolProperty(default=False, options={"SKIP_SAVE"})
gesture: gesture_property()
full_image: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})
preview_fill_rule = 'NONZERO'
_pie_event = None
def _set_status(self, context):
invert = "On" if self.invert else "Off"
context.workspace.status_text_set(
f"Cutout Lasso: drag and release for shapes; "
f"F toggles Invert ({invert}); RMB or Esc cancels"
)
def invoke(self, context, event):
self._pie_event = event
if not self.full_image and event.value != "DOUBLE_CLICK":
return super().invoke(context, event)
source_object, should_edit = resolve_image_edit_click(
context, event, self.report
)
if not should_edit:
return {"FINISHED"} if source_object is not None else {"CANCELLED"}
if source_object is None:
return {"CANCELLED"}
if video_total_frames(source_object.data) > 1:
self.report({"WARNING"}, "Image tools only support a still image")
return {"CANCELLED"}
apply_image_mask_settings(self, context)
self.invert = False
self.source_object_name = source_object.name
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
width, height = (int(value) for value in source_object.data.size)
self.selection_data = json.dumps(
((0, 0), (width, 0), (width, height), (0, height)),
separators=(",", ":"),
)
return self.execute(context)
def modal(self, context, event):
self._pie_event = event
return super().modal(context, event)
def execute(self, context):
source_object = bpy.data.objects.get(self.source_object_name)
if not is_image_empty(source_object):
self.report({"ERROR"}, "The source Image Empty is no longer available")
return {"CANCELLED"}
try:
polygon = json.loads(self.selection_data)
polygon_bounds(polygon)
selected_image_rgba(
source_object, polygon, self.invert, "NONZERO"
)
except (ImageEditWarning, json.JSONDecodeError, TypeError, ValueError) as error:
report_image_edit_exception(self, error)
return {"CANCELLED"}
_open_shape_pie(
context,
self._pie_event,
source_object,
polygon,
self.refine_selection,
self.invert,
)
return {"FINISHED"}
class ActivateCutoutTool(bpy.types.Operator):
bl_idname = "anyimage.activate_cutout_tool"
bl_label = "Cutout Tool"
bl_description = "Activate the Cutout Lasso tool"
bl_options = {"INTERNAL"}
@classmethod
def poll(cls, context):
return context.area is not None and context.area.type == "VIEW_3D"
def execute(self, _context):
return bpy.ops.wm.tool_set_by_id(
name=CutoutTool.bl_idname,
space_type="VIEW_3D",
)
class CutoutTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.cutout_lasso"
bl_label = "Cutout"
bl_description = "Lasso an Image Empty and choose a mesh shape"
bl_icon = "ops.sculpt.lasso_mask"
bl_operator = SelectCutoutRegion.bl_idname
bl_keymap = (
(
SelectCutoutRegion.bl_idname,
{"type": "LEFTMOUSE", "value": "DOUBLE_CLICK"},
{"properties": [("gesture", "LASSO"), ("full_image", True)]},
),
(
SelectCutoutRegion.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "LASSO")]},
),
)
def draw_settings(context, layout, _tool):
settings = context.scene.anyimage_settings
layout.prop(settings, "cutout_point_spacing")
layout.prop(settings, "cutout_normal_analysis")
draw_image_mask_settings(context, layout, "cutout_refine_selection")