Refactor Cutout Tool to Use World Space Edge Length

- Replaced the Mesh Detail parameter with a new Edge Length property to define cutout sampling in physical units.
- Introduced Minimum Relative Edge Length preference to control effective spacing based on the bounding box of the cutout content.
- Updated tests to validate the new edge length functionality and ensure consistent behavior across different resolutions and canvas sizes.
- Removed deprecated parameters and adjusted related properties to align with the new edge length implementation.
- Enhanced geometry sampling logic to utilize effective world spacing for both standard and fine outline modes.
This commit is contained in:
2026-09-12 23:03:24 +08:00
parent 938d95db0f
commit 18799e8828
28 changed files with 505 additions and 279 deletions

View File

@@ -8,12 +8,12 @@ from ...preferences import (
addon_preferences,
configured_geometry_model,
configured_max_ai_input_size,
configured_max_cutout_mesh_resolution,
)
from ...properties import (
cutout_alpha_threshold_property,
cutout_fine_outline_property,
cutout_gesture_property,
cutout_edge_length_property,
)
from ...runtime import JobOperatorBase
from ...server.model_catalog import DEFAULT_GEOMETRY_MODEL_KEY
@@ -59,7 +59,7 @@ from ...common.selection import (
from .geometry import (
CUTOUT_ALPHA_THRESHOLD,
build_base_shape,
limit_cutout_point_spacing,
effective_cutout_edge_length,
)
from .interaction import open_shape_pie
from .object import (
@@ -89,7 +89,7 @@ def create_cutout_shape(
context,
source_object,
shape,
point_spacing,
edge_length,
content_values,
content_bounds,
*,
@@ -101,11 +101,22 @@ def create_cutout_shape(
normal_mode=NormalMode.NONE,
color_image,
gesture="LASSO",
report=None,
):
effective_length = effective_cutout_edge_length(
context, source_object, edge_length, content_values, content_bounds, alpha_threshold
)
if report is not None and effective_length > edge_length * (1 + 1e-6):
units = context.scene.unit_settings
label = bpy.utils.units.to_string(
units.system if units.system != "NONE" else "METRIC", "LENGTH",
effective_length * units.scale_length, precision=4,
)
report({"INFO"}, f"Effective Edge Length: {label}")
base_shape = build_base_shape(
context,
source_object,
point_spacing,
effective_length,
content_values,
content_bounds,
alpha_threshold=alpha_threshold,
@@ -115,7 +126,7 @@ def create_cutout_shape(
context,
source_object,
shape,
point_spacing,
effective_length,
content_values,
content_bounds,
base_shape,
@@ -191,11 +202,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
selection_bounds_json: bpy.props.StringProperty(options=_HIDDEN)
shape: bpy.props.StringProperty(default="SOLID", options=_HIDDEN)
gesture: bpy.props.StringProperty(default="LASSO", options=_HIDDEN)
point_spacing: bpy.props.FloatProperty(
default=32.0,
min=0.01,
options=_HIDDEN,
)
edge_length: cutout_edge_length_property(options=_HIDDEN)
model: bpy.props.StringProperty(
default=DEFAULT_GEOMETRY_MODEL_KEY,
options=_HIDDEN,
@@ -234,13 +241,6 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
source_rgba = image_rgba(source_object.data)
context.view_layer.objects.active = source_object
source_object.select_set(True)
crop_width = selection_mask.bounds[2] - selection_mask.bounds[0]
crop_height = selection_mask.bounds[3] - selection_mask.bounds[1]
self.point_spacing = limit_cutout_point_spacing(
self.point_spacing,
(crop_width, crop_height),
configured_max_cutout_mesh_resolution(context),
)
self.max_ai_input_size = configured_max_ai_input_size(context)
if not self._needs_generation():
left, top, right, bottom = selection_mask.bounds
@@ -251,11 +251,12 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
)
with material_color_image(source_object.data, bounds=selection_mask.bounds, rgba=source_rgba) as color_image:
create_cutout_shape(
context, source_object, self.shape, self.point_spacing,
context, source_object, self.shape, self.edge_length,
content_values, selection_mask.bounds,
alpha_threshold=self.alpha_threshold,
fine_outline=self.fine_outline, color_image=color_image,
gesture=self.gesture,
report=self.report,
)
return {"FINISHED"}
setup_result = invoke_ai_setup_if_needed(self)
@@ -354,7 +355,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
context,
source_object,
self.shape,
self.point_spacing,
self.edge_length,
content_values,
content_bounds,
depth_image=depth_image,
@@ -365,6 +366,7 @@ class CutoutSelectionToShape(JobOperatorBase, bpy.types.Operator):
fine_outline=self.fine_outline,
color_image=color_image,
gesture=self.gesture,
report=self.report,
)
except Exception:
if normal_image is not None and normal_image.users == 0:
@@ -460,7 +462,7 @@ class CutoutTool(bpy.types.WorkSpaceTool):
def draw_settings(context, layout, _tool):
settings = context.scene.anyimage_settings
layout.prop(settings, "cutout_gesture")
layout.prop(settings, "cutout_mesh_detail")
layout.prop(settings, "cutout_edge_length")
layout.prop(settings, "cutout_fine_outline")
layout.prop(settings, "cutout_alpha_threshold")
status = draw_ai_setup(layout)