feat: Update version to 0.13.0 in project files and adjust dependencies

This commit is contained in:
2026-09-02 19:50:55 +08:00
parent 8d279e9ab3
commit a6041cbe03
47 changed files with 1204 additions and 587 deletions

View File

@@ -4,13 +4,12 @@ from .cutout_tool import (
SelectCutoutSelection,
)
from .debug import DebugBen2, DebugDa3, DebugMoge2, DebugUpscale
from .image_tool import CLASSES as IMAGE_TOOL_CLASSES
from .crop_tool import CLASSES as CROP_TOOL_CLASSES
from .convert_to_plane import (
ConvertToDepthPlane,
ConvertToPlane,
GenerateDepthPlane,
)
from .perspective_tool import CLASSES as PERSPECTIVE_CLASSES
from .ai_setup import (
ClearModels,
DownloadModel,
@@ -35,8 +34,7 @@ CLASSES = (
SelectCutoutSelection,
RunBackgroundRemoval,
ClearModels,
*IMAGE_TOOL_CLASSES,
*PERSPECTIVE_CLASSES,
*CROP_TOOL_CLASSES,
RemoveImageBackground,
UpscaleImage,
ConvertToPlane,

View File

@@ -0,0 +1,21 @@
from .box import CropBoxTool
from .lasso import CropLassoTool
from .perspective import CropImagePerspective, CropPerspectiveTool
from .polyline import CropPolylineTool
from .selection import ActivateCropTool, CropImageSelection, RefineCropSelection
CLASSES = (
CropImageSelection,
RefineCropSelection,
ActivateCropTool,
CropImagePerspective,
)
__all__ = (
"CLASSES",
"CropBoxTool",
"CropLassoTool",
"CropPerspectiveTool",
"CropPolylineTool",
)

View File

@@ -0,0 +1,30 @@
import bpy
from ...common.viewport import draw_crop_settings
from .selection import CropImageSelection
class CropBoxTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.crop_box"
bl_label = "Crop Box"
bl_description = "Crop an Image Empty with a rectangular selection"
bl_icon = "ops.sculpt.border_mask"
bl_operator = CropImageSelection.bl_idname
bl_keymap = (
(
CropImageSelection.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "BOX")]},
),
)
def draw_settings(context, layout, _tool):
draw_crop_settings(
context,
layout,
refine_selection="crop_box_refine_selection",
invert="crop_box_invert",
keep_original="crop_box_keep_original",
)

View File

@@ -0,0 +1,30 @@
import bpy
from ...common.viewport import draw_crop_settings
from .selection import CropImageSelection
class CropLassoTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.crop_lasso"
bl_label = "Crop Lasso"
bl_description = "Crop an Image Empty with a freehand selection"
bl_icon = "ops.sculpt.lasso_mask"
bl_operator = CropImageSelection.bl_idname
bl_keymap = (
(
CropImageSelection.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "LASSO")]},
),
)
def draw_settings(context, layout, _tool):
draw_crop_settings(
context,
layout,
refine_selection="crop_lasso_refine_selection",
invert="crop_lasso_invert",
keep_original="crop_lasso_keep_original",
)

View File

@@ -4,8 +4,15 @@ import math
import bpy
from ...common.viewport import (
active_view3d_tool_id,
deserialize_matrix,
draw_crop_settings,
draw_dashed_line,
draw_polygon_fill,
drawing_in_region,
image_edit_poll,
report_image_edit_exception,
resolve_image_edit_click,
screen_path_to_image_pixels,
serialize_matrix,
)
@@ -15,25 +22,15 @@ from ...common.image import (
image_base_name,
image_pixels,
image_empty_bounds,
replace_empty_image,
place_empty_image_result,
require_image_empty,
video_total_frames,
)
from ...common.viewport import (
active_view3d_tool_id,
draw_dashed_line,
draw_polygon_fill,
image_edit_poll,
report_image_edit_exception,
resolve_image_edit_click,
)
MIN_QUAD_AREA = 16.0
MAX_OUTPUT_DIMENSION = 8192
MAX_OUTPUT_PIXELS = 16_777_216
WARP_CHUNK_ROWS = 256
IMAGE_PERSPECTIVE_TOOL_ID = "anyimage.image_perspective"
CROP_PERSPECTIVE_TOOL_ID = "anyimage.crop_perspective"
PREVIEW_TEXTURE_SIZE = 512
ASPECT_PIXELS_PER_DOUBLE = 240.0
MIN_INTERACTIVE_ASPECT = 0.05
@@ -497,14 +494,18 @@ def extract_perspective_pixels(
return pixels, cropped_size, placement_bounds
class EditImagePerspective(bpy.types.Operator):
bl_idname = "anyimage.edit_image_perspective"
bl_label = "Edit Image Perspective"
bl_description = "Deform four image corners into a rectangular image"
class CropImagePerspective(bpy.types.Operator):
bl_idname = "anyimage.crop_image_perspective"
bl_label = "Crop Image Perspective"
bl_description = "Crop four image corners into a rectangular image"
bl_options = {"REGISTER", "UNDO"}
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
source_matrix_data: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
keep_original: bpy.props.BoolProperty(
default=False,
options={"HIDDEN", "SKIP_SAVE"},
)
quad_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
aspect_ratio: bpy.props.FloatProperty(
default=1.0,
@@ -576,14 +577,14 @@ class EditImagePerspective(bpy.types.Operator):
def _set_point_status(self, context):
remaining = 4 - len(self._points)
context.workspace.status_text_set(
f"Image Perspective: click {remaining} more corner"
f"Crop Perspective: click {remaining} more corner"
f"{'s' if remaining != 1 else ''}; click order does not matter; "
"Backspace removes, RMB or Esc cancels"
)
def _set_aspect_status(self, context):
context.workspace.status_text_set(
f"Image Perspective Aspect {self.aspect_ratio:.3f}: "
f"Crop Perspective Aspect {self.aspect_ratio:.3f}: "
"move mouse horizontally, LMB confirms, Backspace returns, "
"RMB or Esc cancels"
)
@@ -639,11 +640,14 @@ class EditImagePerspective(bpy.types.Operator):
if video_total_frames(source_object.data) > 1:
self.report(
{"WARNING"},
"Image Perspective only supports a still image",
"Crop Perspective only supports a still image",
)
return {"CANCELLED"}
self.source_object_name = source_object.name
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
self.keep_original = (
context.scene.anyimage_settings.crop_perspective_keep_original
)
self.quad_json = ""
self._phase = "POINTS"
self._preview_image = None
@@ -665,7 +669,7 @@ class EditImagePerspective(bpy.types.Operator):
def modal(self, context, event):
active_tool = active_view3d_tool_id(context)
if active_tool is not None and active_tool != IMAGE_PERSPECTIVE_TOOL_ID:
if active_tool is not None and active_tool != CROP_PERSPECTIVE_TOOL_ID:
self._finish(context)
return {"CANCELLED"}
if event.type in {"ESC", "RIGHTMOUSE"} and event.value == "PRESS":
@@ -802,7 +806,7 @@ class EditImagePerspective(bpy.types.Operator):
return {"CANCELLED"}
try:
if video_total_frames(source_object.data) > 1:
raise RuntimeError("Image Perspective only supports a still image")
raise RuntimeError("Crop Perspective only supports a still image")
quad = validate_perspective_quad(json.loads(self.quad_json))
if not perspective_quad_overlaps_image(
quad, tuple(source_object.data.size)
@@ -830,10 +834,12 @@ class EditImagePerspective(bpy.types.Operator):
pixels,
output_size,
)
replace_empty_image(
place_empty_image_result(
context,
source_object,
result_image,
placement_bounds=placement_bounds,
keep_original=bool(getattr(self, "keep_original", False)),
)
except (
json.JSONDecodeError,
@@ -847,18 +853,25 @@ class EditImagePerspective(bpy.types.Operator):
return {"FINISHED"}
class ImagePerspectiveTool(bpy.types.WorkSpaceTool):
class CropPerspectiveTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = IMAGE_PERSPECTIVE_TOOL_ID
bl_label = "Image Perspective"
bl_description = "Rectify four image corners on the source Image Empty plane"
bl_idname = CROP_PERSPECTIVE_TOOL_ID
bl_label = "Crop Perspective"
bl_description = "Crop four image corners to a rectangular perspective"
bl_icon = "ops.sculpt.line_project"
bl_operator = EditImagePerspective.bl_idname
bl_operator = CropImagePerspective.bl_idname
bl_keymap = (
(
EditImagePerspective.bl_idname,
CropImagePerspective.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
None,
),
)
def draw_settings(context, layout, _tool):
draw_crop_settings(
context,
layout,
keep_original="crop_perspective_keep_original",
)

View File

@@ -0,0 +1,29 @@
import bpy
from ...common.viewport import draw_crop_settings
from .selection import CropImageSelection
class CropPolylineTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.crop_polyline"
bl_label = "Crop Polyline"
bl_description = "Crop an Image Empty with a clicked polygon selection"
bl_icon = "ops.sculpt.polyline_mask"
bl_operator = CropImageSelection.bl_idname
bl_keymap = (
(
CropImageSelection.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "POLYLINE")]},
),
)
def draw_settings(context, layout, _tool):
draw_crop_settings(
context,
layout,
invert="crop_polyline_invert",
keep_original="crop_polyline_keep_original",
)

View File

@@ -15,13 +15,13 @@ from ...common.image import (
image_base_name,
image_rgba,
load_packed_rgba_image,
place_empty_image_result,
prepare_image_bounds_input,
replace_empty_image,
require_image_empty,
video_total_frames,
)
from ...common.viewport import (
ImageSelectionGesture,
CropSelectionGesture,
gesture_property,
image_edit_poll,
report_image_edit_exception,
@@ -64,7 +64,14 @@ def _edit_image_mask_pixels(
)
def _edit_image_object(source_object, source_rgba, selection_mask):
def _edit_image_object(
context,
source_object,
source_rgba,
selection_mask,
*,
keep_original=False,
):
image = source_object.data
pixels, crop_bounds = _edit_image_mask_pixels(
source_rgba,
@@ -76,21 +83,33 @@ def _edit_image_object(source_object, source_rgba, selection_mask):
pixels,
(right - left, bottom - top),
)
return replace_empty_image(
return place_empty_image_result(
context,
source_object,
result_image,
placement_bounds=crop_bounds,
keep_original=keep_original,
)
class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
class CropImageSelection(CropSelectionGesture, bpy.types.Operator):
refine_selection_settings = {
"BOX": "image_box_refine_selection",
"LASSO": "image_lasso_refine_selection",
"BOX": "crop_box_refine_selection",
"LASSO": "crop_lasso_refine_selection",
}
bl_idname = "anyimage.edit_image_mask"
bl_label = "Edit Image Mask"
bl_description = "Edit and tightly crop an Image Empty with a gesture"
invert_settings = {
"BOX": "crop_box_invert",
"LASSO": "crop_lasso_invert",
"POLYLINE": "crop_polyline_invert",
}
keep_original_settings = {
"BOX": "crop_box_keep_original",
"LASSO": "crop_lasso_keep_original",
"POLYLINE": "crop_polyline_keep_original",
}
bl_idname = "anyimage.crop_image_selection"
bl_label = "Crop Image Selection"
bl_description = "Crop an Image Empty with a selection"
bl_options = {"REGISTER", "UNDO"}
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
@@ -100,11 +119,13 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
name="Invert",
description="Keep the opposite side of the refined gesture mask",
default=False,
options={"SKIP_SAVE"},
options=_HIDDEN,
)
keep_original: bpy.props.BoolProperty(default=False, options=_HIDDEN)
gesture: gesture_property()
def _submit_selection_path(self, context, selection_path):
keep_original = bool(getattr(self, "keep_original", False))
try:
source_object = require_image_empty(self.source_object_name)
except RuntimeError as error:
@@ -112,7 +133,7 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
return {"CANCELLED"}
try:
if video_total_frames(source_object.data) > 1:
raise RuntimeError("Image tools only support a still image")
raise RuntimeError("Crop tools only support a still image")
selection_mask = rasterize_selection_path(
tuple(source_object.data.size),
selection_path,
@@ -124,19 +145,22 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
source_rgba,
selection_mask.bounds,
)
result = bpy.ops.anyimage.refine_image_selection(
result = bpy.ops.anyimage.refine_crop_selection(
input_path=str(source_path),
source_object_name=self.source_object_name,
selection_bounds_json=json.dumps(selection_mask.bounds),
keep_original=keep_original,
)
if "CANCELLED" in result:
cleanup_image_input(source_path, True)
return {"CANCELLED"}
return {"FINISHED"}
_edit_image_object(
context,
source_object,
source_rgba,
selection_mask,
keep_original=keep_original,
)
except (RuntimeError, TypeError, ValueError) as error:
report_image_edit_exception(self, error)
@@ -144,15 +168,16 @@ class EditImageMask(ImageSelectionGesture, bpy.types.Operator):
return {"FINISHED"}
class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.refine_image_selection"
bl_label = "Refine Image Selection"
class RefineCropSelection(JobOperatorBase, bpy.types.Operator):
bl_idname = "anyimage.refine_crop_selection"
bl_label = "Refine Crop Selection"
job_type = "refine-image-selection"
starting_message = "Refining image selection"
input_path: bpy.props.StringProperty(options=_HIDDEN)
source_object_name: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
selection_bounds_json: bpy.props.StringProperty(options={"HIDDEN", "SKIP_SAVE"})
keep_original: bpy.props.BoolProperty(default=False, options=_HIDDEN)
def request(self, _context):
require_environment()
@@ -163,7 +188,7 @@ class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
"device": production_device().lower(),
}
def response(self, _context, result):
def response(self, context, result):
source_object = require_image_empty(self.source_object_name)
source_bounds = tuple(json.loads(self.selection_bounds_json))
@@ -183,27 +208,29 @@ class RefineImageSelection(JobOperatorBase, bpy.types.Operator):
image_base_name(source_image),
)
try:
replace_empty_image(
place_empty_image_result(
context,
source_object,
refined_image,
placement_bounds=bounds,
keep_original=bool(getattr(self, "keep_original", False)),
)
except Exception:
if refined_image.users == 0:
bpy.data.images.remove(refined_image, do_unlink=True)
raise
if "FINISHED" not in bpy.ops.ed.undo_push(message="Edit Image Mask"):
raise RuntimeError("Unable to create the Edit Image Mask Undo step")
return "Image Selection refined"
if "FINISHED" not in bpy.ops.ed.undo_push(message="Crop Image Selection"):
raise RuntimeError("Unable to create the Crop Image Selection Undo step")
return "Crop Selection refined"
def cleanup(self):
cleanup_image_input(self.input_path, True)
class ActivateImageTool(bpy.types.Operator):
bl_idname = "anyimage.activate_image_tool"
bl_label = "Image Tool"
bl_description = "Activate the Image Box tool in the 3D Viewport"
class ActivateCropTool(bpy.types.Operator):
bl_idname = "anyimage.activate_crop_tool"
bl_label = "Crop Tool"
bl_description = "Activate Crop Box in the 3D Viewport"
bl_options = {"INTERNAL"}
@classmethod
@@ -212,5 +239,5 @@ class ActivateImageTool(bpy.types.Operator):
def execute(self, _context):
return bpy.ops.wm.tool_set_by_id(
name="anyimage.image_box", space_type="VIEW_3D"
name="anyimage.crop_box", space_type="VIEW_3D"
)

View File

@@ -45,6 +45,7 @@ def _pie_button(
refine_selection,
generate_normal,
mesh_resolution,
inward_axis,
):
operator = pie.operator(
"anyimage.cutout_selection_to_shape",
@@ -57,6 +58,7 @@ def _pie_button(
operator.refine_selection = refine_selection
operator.generate_normal = generate_normal
operator.mesh_resolution = mesh_resolution
operator.inward_axis = inward_axis
def open_shape_pie(
@@ -76,6 +78,7 @@ def open_shape_pie(
getattr(settings, "cutout_generate_normal", False)
)
mesh_resolution = cutout_mesh_resolution(source_object.data.size, point_spacing)
inward_axis = str(getattr(settings, "cutout_inward_axis", "NEGATIVE_Z"))
def draw(menu, _context):
pie = menu.layout.menu_pie()
@@ -91,6 +94,7 @@ def open_shape_pie(
refine_selection,
generate_normal,
mesh_resolution,
inward_axis,
)
context.window_manager.popup_menu_pie(

View File

@@ -45,8 +45,8 @@ from ...common.image import (
)
from ...common.depth import load_depth_metadata, load_depth_result_image
from ...common.viewport import (
ImageSelectionGesture,
apply_selection_settings,
CropSelectionGesture,
apply_crop_settings,
gesture_property,
report_image_edit_exception,
resolve_image_edit_click,
@@ -64,9 +64,11 @@ from .geometry import (
)
from .interaction import open_shape_pie
from .object import (
CutoutInwardAxis,
NormalMode,
create_cropped_color_image,
create_shape_object,
orient_object_normal_image,
)
from .shape import CUTOUT_SHAPES, DEPTH_CUTOUT_SHAPES
@@ -101,6 +103,7 @@ def create_cutout_shape(
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
color_image,
):
try:
@@ -123,6 +126,7 @@ def create_cutout_shape(
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
color_image=color_image,
)
except Exception:
@@ -235,6 +239,10 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
)
generate_normal: bpy.props.BoolProperty(default=False, options=_HIDDEN)
refine_selection: bpy.props.BoolProperty(default=False, options=_HIDDEN)
inward_axis: bpy.props.StringProperty(
default=CutoutInwardAxis.NEGATIVE_Z.value,
options=_HIDDEN,
)
def _needs_generation(self):
return (
@@ -245,6 +253,11 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
def execute(self, context):
try:
self.inward_axis = getattr(
self,
"inward_axis",
CutoutInwardAxis.NEGATIVE_Z.value,
)
source_object = require_image_empty(self.source_object_name)
if video_total_frames(source_object.data) != 1:
raise ValueError("Cutout currently supports still images")
@@ -285,6 +298,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
self.mesh_resolution,
content_values,
selection_mask.bounds,
inward_axis=self.inward_axis,
color_image=color_image,
)
return {"FINISHED"}
@@ -355,6 +369,11 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
return parameters
def response(self, context, result):
inward_axis = getattr(
self,
"inward_axis",
CutoutInwardAxis.NEGATIVE_Z.value,
)
source_object = require_image_empty(self.source_object_name)
source_bounds = tuple(json.loads(self.selection_bounds_json))
color_image, source_alpha, color_bounds = _generated_color_result(
@@ -389,6 +408,8 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
result.file(normal_key),
source_object,
)
if normal_mode == NormalMode.OBJECT:
orient_object_normal_image(normal_image, inward_axis)
depth_metadata = None
depth_mode = depth_mode_for_shape(self.shape)
if depth_mode != "NONE":
@@ -410,6 +431,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
depth_metadata=depth_metadata,
normal_image=normal_image,
normal_mode=normal_mode,
inward_axis=inward_axis,
color_image=color_image,
)
except Exception:
@@ -426,7 +448,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
cleanup_image_input(self.input_path, True)
class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
class SelectCutoutSelection(CropSelectionGesture, bpy.types.Operator):
bl_idname = "anyimage.select_cutout_selection"
bl_label = "Start Cutout Selection"
bl_description = "Lasso an Image Empty selection, then choose its mesh shape"
@@ -444,10 +466,8 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
_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"
"Cutout Lasso: drag and release for shapes; RMB or Esc cancels"
)
def invoke(self, context, event):
@@ -462,9 +482,9 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
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")
self.report({"WARNING"}, "Cutout only supports a still image")
return {"CANCELLED"}
apply_selection_settings(self, context)
apply_crop_settings(self, context)
self.invert = False
self.source_object_name = source_object.name
self.source_matrix_data = serialize_matrix(source_object.matrix_world)
@@ -476,7 +496,7 @@ class SelectCutoutSelection(ImageSelectionGesture, bpy.types.Operator):
def modal(self, context, event):
self._pie_event = event
return super().modal(context, event)
return CropSelectionGesture.modal(self, context, event)
def _submit_selection_path(self, context, selection_path):
try:
@@ -500,7 +520,7 @@ class CutoutTool(bpy.types.WorkSpaceTool):
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_icon = "ops.mesh.primitive_sphere_add_gizmo"
bl_operator = SelectCutoutSelection.bl_idname
bl_keymap = (
(
@@ -521,3 +541,4 @@ class CutoutTool(bpy.types.WorkSpaceTool):
draw_ai_property(layout, settings, "cutout_refine_selection", status)
draw_ai_property(layout, settings, "cutout_generate_normal", status)
layout.prop(settings, "cutout_mesh_detail")
layout.prop(settings, "cutout_inward_axis")

View File

@@ -31,6 +31,7 @@ from .geometry import (
from .shape import (
BALLOON_ATTRIBUTE_NAME,
BALLOON_CUTOUT_SHAPES,
CUTOUT_AXIS_TO_SOURCE,
CUTOUT_NODE_GROUP_NAME,
CUTOUT_SHAPE_INDICES,
DEPTH_CUTOUT_SHAPES,
@@ -43,23 +44,38 @@ class NormalMode(StrEnum):
OBJECT = "OBJECT"
CUTOUT_AXIS_TO_SOURCE = (
(0.0, -1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(-1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
class CutoutInwardAxis(StrEnum):
NEGATIVE_Z = "NEGATIVE_Z"
POSITIVE_X = "POSITIVE_X"
def _cutout_world_matrix(source_matrix, crop_center):
def _cutout_world_matrix(source_matrix, crop_center, inward_axis):
from mathutils import Matrix
center_x, center_y = (float(value) for value in crop_center)
return (
source_matrix
@ Matrix.Translation((center_x, center_y, 0.0))
@ Matrix(CUTOUT_AXIS_TO_SOURCE)
)
matrix = source_matrix @ Matrix.Translation((center_x, center_y, 0.0))
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
matrix @= Matrix(CUTOUT_AXIS_TO_SOURCE)
return matrix
def orient_object_normal_image(image, inward_axis):
"""Convert internal +X Cutout normals to the selected object axes."""
if CutoutInwardAxis(inward_axis) == CutoutInwardAxis.POSITIVE_X:
return image
import numpy as np
pixels = np.empty(int(image.size[0]) * int(image.size[1]) * 4, dtype=np.float32)
image.pixels.foreach_get(pixels)
rgba = pixels.reshape((-1, 4))
normal = rgba[:, :3] * 2.0 - 1.0
rgba[:, :3] = np.column_stack(
(-normal[:, 1], normal[:, 2], -normal[:, 0])
) * 0.5 + 0.5
image.pixels.foreach_set(pixels)
image.update()
image.pack()
return image
def _assign_uv(mesh, uv):
@@ -149,11 +165,13 @@ def create_shape_object(
depth_metadata=None,
normal_image=None,
normal_mode=NormalMode.NONE,
inward_axis=CutoutInwardAxis.NEGATIVE_Z,
color_image,
):
import numpy as np
normal_mode = NormalMode(normal_mode)
inward_axis = CutoutInwardAxis(inward_axis)
bounds = crop_plane_bounds(
source_object,
content_bounds,
@@ -245,6 +263,7 @@ def create_shape_object(
mesh_object.matrix_world = _cutout_world_matrix(
source_object.matrix_world,
crop_center,
inward_axis,
)
mesh_object["anyimage_mesh_shape"] = shape
node_group = load_cutout_node_group()
@@ -253,6 +272,10 @@ def create_shape_object(
modifier.show_group_selector = False
for name, value in (
("Shape", CUTOUT_SHAPE_INDICES[shape]),
(
"Inward Axis",
0 if inward_axis == CutoutInwardAxis.NEGATIVE_Z else 1,
),
("Cleanup", cleanup_value),
("Smooth Iterations", 0),
("Smooth", 1.0),

View File

@@ -2,6 +2,12 @@
BALLOON_ATTRIBUTE_NAME = "o_balloon"
CUTOUT_NODE_GROUP_NAME = "O Image Cutout"
CUTOUT_AXIS_TO_SOURCE = (
(0.0, -1.0, 0.0, 0.0),
(0.0, 0.0, 1.0, 0.0),
(-1.0, 0.0, 0.0, 0.0),
(0.0, 0.0, 0.0, 1.0),
)
CUTOUT_NODE_GROUP_NAMES = {
"SURFACE": "O Image Surface",
"BALLOON": "O Image Balloon",

View File

@@ -1,19 +0,0 @@
from .box import ImageBoxTool
from .edit_mask import ActivateImageTool, EditImageMask, RefineImageSelection
from .lasso import ImageLassoTool
from .line import ImageLineTool
from .polyline import ImagePolylineTool
CLASSES = (
EditImageMask,
RefineImageSelection,
ActivateImageTool,
)
__all__ = (
"CLASSES",
"ImageBoxTool",
"ImageLassoTool",
"ImageLineTool",
"ImagePolylineTool",
)

View File

@@ -1,24 +0,0 @@
import bpy
from ...common.viewport import draw_selection_settings
from .edit_mask import EditImageMask
class ImageBoxTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.image_box"
bl_label = "Image Box"
bl_description = "Edit an Image Empty with a rectangular mask"
bl_icon = "ops.sculpt.border_mask"
bl_operator = EditImageMask.bl_idname
bl_keymap = (
(
EditImageMask.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "BOX")]},
),
)
def draw_settings(context, layout, _tool):
draw_selection_settings(context, layout, "image_box_refine_selection")

View File

@@ -1,24 +0,0 @@
import bpy
from ...common.viewport import draw_selection_settings
from .edit_mask import EditImageMask
class ImageLassoTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.image_lasso"
bl_label = "Image Lasso"
bl_description = "Edit an Image Empty with a freehand mask"
bl_icon = "ops.sculpt.lasso_mask"
bl_operator = EditImageMask.bl_idname
bl_keymap = (
(
EditImageMask.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "LASSO")]},
),
)
def draw_settings(context, layout, _tool):
draw_selection_settings(context, layout, "image_lasso_refine_selection")

View File

@@ -1,20 +0,0 @@
import bpy
from .edit_mask import EditImageMask
class ImageLineTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.image_line"
bl_label = "Image Line"
bl_description = "Edit one side of an Image Empty with a line"
bl_icon = "ops.sculpt.line_mask"
bl_operator = EditImageMask.bl_idname
bl_keymap = (
(
EditImageMask.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "LINE")]},
),
)

View File

@@ -1,20 +0,0 @@
import bpy
from .edit_mask import EditImageMask
class ImagePolylineTool(bpy.types.WorkSpaceTool):
bl_space_type = "VIEW_3D"
bl_context_mode = "OBJECT"
bl_idname = "anyimage.image_polyline"
bl_label = "Image Polyline"
bl_description = "Edit an Image Empty with a clicked polygon mask"
bl_icon = "ops.sculpt.polyline_mask"
bl_operator = EditImageMask.bl_idname
bl_keymap = (
(
EditImageMask.bl_idname,
{"type": "LEFTMOUSE", "value": "PRESS"},
{"properties": [("gesture", "POLYLINE")]},
),
)

View File

@@ -1,6 +0,0 @@
from .edit_perspective import EditImagePerspective, ImagePerspectiveTool
CLASSES = (EditImagePerspective,)
__all__ = ("CLASSES", "ImagePerspectiveTool")