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

@@ -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",