- 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.
257 lines
7.7 KiB
Python
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")
|