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:
Binary file not shown.
@@ -78,7 +78,7 @@ def support_flat_regions(points, faces):
|
||||
return np.asarray(vertices), np.asarray(triangles, dtype=np.int32)
|
||||
|
||||
|
||||
def build_fine_mesh(alpha, spacing, threshold):
|
||||
def build_fine_mesh(alpha, spacing, threshold, *, pixel_transform=None):
|
||||
"""Return pixel-space triangles and component ranges for height solving."""
|
||||
alpha = np.asarray(alpha, dtype=float)
|
||||
if alpha.ndim != 2 or not alpha.size or not np.isfinite(alpha).all():
|
||||
@@ -92,18 +92,24 @@ def build_fine_mesh(alpha, spacing, threshold):
|
||||
reference = g.extract_selection_contours(
|
||||
filtered, max_resolution=max(mask.shape), threshold=threshold
|
||||
)
|
||||
width_sizes = WidthSamples(mask)
|
||||
# Normalize geometric tolerances and support sampling to the target length.
|
||||
transform = (
|
||||
np.eye(2) if pixel_transform is None else np.asarray(pixel_transform)
|
||||
) * (32 / spacing)
|
||||
inverse = np.linalg.inv(transform)
|
||||
width_sizes = WidthSamples(mask, transform=transform)
|
||||
spacing = 32.0
|
||||
points, faces, components = [], [], []
|
||||
offset = 0
|
||||
for component in reference.components:
|
||||
contours = contours_for_component(component, spacing)
|
||||
contours = contours_for_component([np.asarray(c) @ transform for c in component], spacing)
|
||||
spine, path_edges = interior_paths(
|
||||
contours, min(32, max(mask.shape) / 6), width_sizes
|
||||
contours, spacing, width_sizes
|
||||
)
|
||||
contours, spine, path_edges = subdivide_constraints(
|
||||
contours, spine, path_edges, spacing
|
||||
)
|
||||
free = relax_points(contours, spine, mask.shape, spacing)
|
||||
free = relax_points(contours, spine, spacing)
|
||||
vertices, triangles, constraints = triangulate(
|
||||
contours, spine, path_edges, free, spacing
|
||||
)
|
||||
@@ -117,9 +123,9 @@ def build_fine_mesh(alpha, spacing, threshold):
|
||||
raise ValueError(
|
||||
"Fine Outline placed an interior constraint on the boundary"
|
||||
)
|
||||
points.append(vertices)
|
||||
points.append(vertices @ inverse)
|
||||
faces.append(triangles + offset)
|
||||
components.append((offset, offset + len(vertices), triangles, contours[0]))
|
||||
components.append((offset, offset + len(vertices), triangles, contours[0] @ inverse))
|
||||
offset += len(vertices)
|
||||
points, faces = np.concatenate(points), np.concatenate(faces)
|
||||
return points, faces, g.boundary_vertices_from_faces(len(points), faces), components
|
||||
@@ -181,7 +187,10 @@ def width_ridge_samples(image):
|
||||
class WidthSamples:
|
||||
"""Estimate local width from indexed distance-ridge disks."""
|
||||
|
||||
def __init__(self, mask):
|
||||
def __init__(self, mask, transform=None):
|
||||
transform = np.eye(2) if transform is None else transform
|
||||
self.inverse = np.linalg.inv(transform)
|
||||
self.size_scale = np.linalg.svd(transform, compute_uv=False).min()
|
||||
ridge, distance = width_ridge_samples(np.pad(mask, 1))
|
||||
ridge = ridge[1:-1, 1:-1]
|
||||
self.distance = np.maximum(distance[1:-1, 1:-1] - 0.5, 0.3)
|
||||
@@ -198,6 +207,7 @@ class WidthSamples:
|
||||
)
|
||||
|
||||
def __call__(self, points):
|
||||
points = points @ self.inverse
|
||||
radius = np.full(len(points), 0.5)
|
||||
pending = np.arange(len(points))
|
||||
for ids, tree, reach in self.groups:
|
||||
@@ -226,7 +236,7 @@ class WidthSamples:
|
||||
mode="nearest",
|
||||
)
|
||||
return np.maximum(
|
||||
0.7, np.minimum(32, np.minimum(16, radius * 0.9) + 0.4 * distance)
|
||||
0.7, np.minimum(32, np.minimum(16, radius * self.size_scale * 0.9) + 0.4 * distance * self.size_scale)
|
||||
)
|
||||
|
||||
|
||||
@@ -486,7 +496,7 @@ def interior_paths(contours, spacing, width_sizes):
|
||||
return (np.asarray(points).reshape(-1, 2), edges)
|
||||
|
||||
|
||||
def relax_points(contours, spine, shape, spacing):
|
||||
def relax_points(contours, spine, spacing):
|
||||
free = g.uniform_interior_points_for_contours(contours, spacing)
|
||||
if len(free) and len(spine):
|
||||
free = free[cKDTree(spine).query(free)[0] > spacing * 0.55]
|
||||
@@ -495,8 +505,8 @@ def relax_points(contours, spine, shape, spacing):
|
||||
points = np.concatenate(contours)
|
||||
minimum = points.min(axis=0)
|
||||
maximum = points.max(axis=0)
|
||||
xx = np.arange(phase, shape[1], step) + 0.5
|
||||
yy = np.arange(phase, shape[0], step) + 0.5
|
||||
xx = np.arange(minimum[0] + phase, maximum[0], step) + 0.5
|
||||
yy = np.arange(minimum[1] + phase, maximum[1], step) + 0.5
|
||||
xx = xx[(xx >= minimum[0]) & (xx <= maximum[0])]
|
||||
yy = yy[(yy >= minimum[1]) & (yy <= maximum[1])]
|
||||
xx, yy = np.meshgrid(xx, yy)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ...preferences import addon_preferences
|
||||
from ...preferences import addon_preferences, configured_min_cutout_relative_edge_length
|
||||
from ...common.image import image_empty_bounds
|
||||
from . import balloon
|
||||
|
||||
@@ -824,11 +824,9 @@ def crop_plane_bounds(source_object, crop_bounds, source_size):
|
||||
|
||||
def extract_cutout_contours(
|
||||
content_values,
|
||||
point_spacing,
|
||||
sampling_resolution,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
):
|
||||
height, width = content_values.shape
|
||||
sampling_resolution = max(width, height) / float(point_spacing)
|
||||
contour_resolution = max(512, min(round(sampling_resolution * 8), 1024))
|
||||
return extract_selection_contours(
|
||||
content_values,
|
||||
@@ -837,28 +835,28 @@ def extract_cutout_contours(
|
||||
)
|
||||
|
||||
|
||||
def limit_cutout_point_spacing(point_spacing, crop_size, max_mesh_resolution):
|
||||
crop_width, crop_height = (int(value) for value in crop_size)
|
||||
spacing = float(point_spacing)
|
||||
if crop_width <= 0 or crop_height <= 0:
|
||||
raise ValueError("The Cutout bounds must be positive")
|
||||
if spacing <= 0.0:
|
||||
raise ValueError("Cutout point spacing must be greater than zero")
|
||||
maximum = int(max_mesh_resolution)
|
||||
if maximum <= 0:
|
||||
raise ValueError("Maximum Cutout mesh resolution must be positive")
|
||||
return max(spacing, max(crop_width, crop_height) / maximum)
|
||||
def effective_cutout_edge_length(
|
||||
context, source_object, edge_length, content_values, content_bounds,
|
||||
threshold=CUTOUT_ALPHA_THRESHOLD,
|
||||
):
|
||||
"""Resolve target spacing in Blender world units from visible content bounds."""
|
||||
import numpy as np
|
||||
|
||||
|
||||
def cutout_sampling_spacing(source_object, point_spacing):
|
||||
import math
|
||||
|
||||
x_min, x_max, y_min, y_max = image_empty_bounds(source_object)
|
||||
metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
width = (x_max - x_min) * math.sqrt(max(metric[0][0], 0.0))
|
||||
height = (y_max - y_min) * math.sqrt(max(metric[1][1], 0.0))
|
||||
image_width, image_height = (int(value) for value in source_object.data.size)
|
||||
return max(width / image_width, height / image_height) * float(point_spacing)
|
||||
if not math.isfinite(edge_length) or edge_length <= 0:
|
||||
raise ValueError("Cutout edge length must be greater than zero")
|
||||
values = np.asarray(content_values)
|
||||
visible = (values > 0) & (values >= threshold)
|
||||
rows = np.flatnonzero(visible.any(axis=1))
|
||||
columns = np.flatnonzero(visible.any(axis=0))
|
||||
if not len(rows):
|
||||
raise ValueError("The Cutout selection contains no visible image pixels")
|
||||
bounds = crop_plane_bounds(source_object, content_bounds, tuple(source_object.data.size))
|
||||
metric, _ = world_plane_metric(source_object.matrix_world)
|
||||
metric_transform(metric)
|
||||
width = (bounds[1] - bounds[0]) * (columns.max() + 1 - columns.min()) / values.shape[1]
|
||||
height = (bounds[3] - bounds[2]) * (rows.max() + 1 - rows.min()) / values.shape[0]
|
||||
longest = max(width * math.sqrt(metric[0][0]), height * math.sqrt(metric[1][1]))
|
||||
return max(float(edge_length), longest * configured_min_cutout_relative_edge_length(context))
|
||||
|
||||
|
||||
def selection_calibration(content_values, target_shape, threshold=CUTOUT_ALPHA_THRESHOLD):
|
||||
@@ -885,7 +883,7 @@ def selection_calibration(content_values, target_shape, threshold=CUTOUT_ALPHA_T
|
||||
def build_base_shape(
|
||||
context,
|
||||
source_object,
|
||||
point_spacing,
|
||||
edge_length,
|
||||
content_values,
|
||||
content_bounds,
|
||||
*,
|
||||
@@ -899,31 +897,38 @@ def build_base_shape(
|
||||
content_bounds,
|
||||
tuple(source_object.data.size),
|
||||
)
|
||||
world_metric, world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
if world_z_scale <= 1e-10:
|
||||
raise ValueError("The Image Empty has a degenerate world transform")
|
||||
pixel_transform = np.diag((
|
||||
(bounds[1] - bounds[0]) / content_values.shape[1],
|
||||
-(bounds[3] - bounds[2]) / content_values.shape[0],
|
||||
)) @ metric_transform(world_metric)
|
||||
longest = max(
|
||||
np.linalg.norm(pixel_transform[0]) * content_values.shape[1],
|
||||
np.linalg.norm(pixel_transform[1]) * content_values.shape[0],
|
||||
)
|
||||
fine_mesh = None
|
||||
selection_contours = None
|
||||
if fine_outline:
|
||||
from .fine_mesh import build_fine_mesh
|
||||
|
||||
fine_mesh = (
|
||||
*build_fine_mesh(content_values, point_spacing, alpha_threshold),
|
||||
*build_fine_mesh(content_values, edge_length, alpha_threshold, pixel_transform=pixel_transform),
|
||||
(content_values.shape[1], content_values.shape[0]),
|
||||
)
|
||||
else:
|
||||
selection_contours = extract_cutout_contours(
|
||||
content_values,
|
||||
point_spacing,
|
||||
longest / edge_length,
|
||||
threshold=alpha_threshold,
|
||||
)
|
||||
world_metric, world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
if world_z_scale <= 1e-10:
|
||||
raise ValueError("The Image Empty has a degenerate world transform")
|
||||
preferences = addon_preferences(context)
|
||||
balloon_algorithm = getattr(preferences, "balloon_profile_algorithm", "POISSON")
|
||||
point_spacing = cutout_sampling_spacing(source_object, point_spacing)
|
||||
domain = cutout_domain(
|
||||
selection_contours,
|
||||
bounds,
|
||||
point_spacing,
|
||||
edge_length,
|
||||
world_metric,
|
||||
balloon_algorithm,
|
||||
fine_mesh=fine_mesh,
|
||||
|
||||
@@ -10,17 +10,6 @@ CUTOUT_SHAPE_BUTTONS = (
|
||||
("DEPTH_SOLID", "SURFACE_NCURVE"),
|
||||
)
|
||||
|
||||
CUTOUT_MESH_DETAIL_SPACING = {
|
||||
"LOW": 32,
|
||||
"MEDIUM": 16,
|
||||
"HIGH": 8,
|
||||
"ULTRA": 4,
|
||||
}
|
||||
|
||||
|
||||
def cutout_mesh_detail_spacing(mesh_detail):
|
||||
return CUTOUT_MESH_DETAIL_SPACING[str(mesh_detail)]
|
||||
|
||||
|
||||
def _pie_button(
|
||||
pie,
|
||||
@@ -30,7 +19,7 @@ def _pie_button(
|
||||
source_name,
|
||||
selection_path_json,
|
||||
generate_normal,
|
||||
point_spacing,
|
||||
edge_length,
|
||||
alpha_threshold,
|
||||
fine_outline,
|
||||
gesture,
|
||||
@@ -44,7 +33,7 @@ def _pie_button(
|
||||
operator.selection_path_json = selection_path_json
|
||||
operator.shape = shape
|
||||
operator.generate_normal = generate_normal
|
||||
operator.point_spacing = point_spacing
|
||||
operator.edge_length = edge_length
|
||||
operator.alpha_threshold = alpha_threshold
|
||||
operator.fine_outline = fine_outline
|
||||
operator.gesture = gesture
|
||||
@@ -61,7 +50,7 @@ def open_shape_pie(
|
||||
source_name = source_object.name
|
||||
selection_path_json = selection_path.to_json()
|
||||
settings = context.scene.anyimage_settings
|
||||
point_spacing = cutout_mesh_detail_spacing(settings.cutout_mesh_detail)
|
||||
edge_length = settings.cutout_edge_length
|
||||
ready = ai_ready()
|
||||
generate_normal = ready and bool(
|
||||
getattr(settings, "cutout_generate_normal", False)
|
||||
@@ -81,7 +70,7 @@ def open_shape_pie(
|
||||
source_name,
|
||||
selection_path_json,
|
||||
generate_normal,
|
||||
point_spacing,
|
||||
edge_length,
|
||||
alpha_threshold,
|
||||
fine_outline,
|
||||
gesture,
|
||||
|
||||
@@ -26,7 +26,6 @@ from .geometry import (
|
||||
CUTOUT_ALPHA_THRESHOLD,
|
||||
crop_plane_bounds,
|
||||
cutout_cleanup_value,
|
||||
cutout_sampling_spacing,
|
||||
selection_calibration,
|
||||
world_plane_metric,
|
||||
)
|
||||
@@ -98,7 +97,7 @@ def create_shape_object(
|
||||
context,
|
||||
source_object,
|
||||
shape,
|
||||
point_spacing,
|
||||
edge_length,
|
||||
content_values,
|
||||
content_bounds,
|
||||
base_shape,
|
||||
@@ -125,7 +124,7 @@ def create_shape_object(
|
||||
)
|
||||
world_metric, _world_z_scale = world_plane_metric(source_object.matrix_world)
|
||||
cleanup_value = cutout_cleanup_value(
|
||||
cutout_sampling_spacing(source_object, point_spacing),
|
||||
edge_length,
|
||||
world_metric,
|
||||
)
|
||||
material = None
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -16,9 +16,6 @@ from .server.model_catalog import (
|
||||
UPSCALE_MODELS,
|
||||
)
|
||||
|
||||
DEFAULT_MAX_CUTOUT_MESH_RESOLUTION = 1024
|
||||
MIN_CUTOUT_MESH_RESOLUTION = 4
|
||||
MAX_CUTOUT_MESH_RESOLUTION = 1024
|
||||
DEFAULT_MAX_FRAME_RESOLUTION = 2048
|
||||
MIN_FRAME_RESOLUTION = 64
|
||||
MAX_FRAME_RESOLUTION = 8192
|
||||
@@ -87,19 +84,9 @@ def configured_material_view_adaptation(context=None):
|
||||
)
|
||||
|
||||
|
||||
def configured_max_cutout_mesh_resolution(context=None):
|
||||
preferences = addon_preferences(context)
|
||||
value = int(
|
||||
getattr(
|
||||
preferences,
|
||||
"max_cutout_mesh_resolution",
|
||||
DEFAULT_MAX_CUTOUT_MESH_RESOLUTION,
|
||||
)
|
||||
)
|
||||
return min(
|
||||
max(value, MIN_CUTOUT_MESH_RESOLUTION),
|
||||
MAX_CUTOUT_MESH_RESOLUTION,
|
||||
)
|
||||
def configured_min_cutout_relative_edge_length(context=None):
|
||||
percent = float(getattr(addon_preferences(context), "min_cutout_relative_edge_length", 1.0))
|
||||
return min(max(percent, 0.001), 100.0) / 100.0
|
||||
|
||||
|
||||
def configured_max_frame_resolution(context=None):
|
||||
@@ -177,17 +164,13 @@ class AnyImagePreferences(bpy.types.AddonPreferences):
|
||||
step=256,
|
||||
subtype="PIXEL",
|
||||
)
|
||||
max_cutout_mesh_resolution: bpy.props.IntProperty(
|
||||
name="Maximum Cutout Mesh Resolution",
|
||||
description=(
|
||||
"Limit the longest image edge used to sample the Cutout mesh. "
|
||||
"Higher values allow finer outlines and denser meshes."
|
||||
),
|
||||
default=DEFAULT_MAX_CUTOUT_MESH_RESOLUTION,
|
||||
min=MIN_CUTOUT_MESH_RESOLUTION,
|
||||
max=MAX_CUTOUT_MESH_RESOLUTION,
|
||||
step=64,
|
||||
subtype="PIXEL",
|
||||
min_cutout_relative_edge_length: bpy.props.FloatProperty(
|
||||
name="Minimum Relative Edge Length",
|
||||
description="Minimum target edge length as a percentage of the Cutout bounds' longest side",
|
||||
default=1.0,
|
||||
min=0.001,
|
||||
max=100.0,
|
||||
subtype="PERCENTAGE",
|
||||
)
|
||||
max_frame_resolution: bpy.props.IntProperty(
|
||||
name="Maximum Frame Resolution",
|
||||
@@ -292,7 +275,7 @@ class AnyImagePreferences(bpy.types.AddonPreferences):
|
||||
tools.prop(self, "geometry_resolution_level")
|
||||
tools.separator(type="LINE")
|
||||
tools.label(text="Cutout Tool")
|
||||
tools.prop(self, "max_cutout_mesh_resolution")
|
||||
tools.prop(self, "min_cutout_relative_edge_length")
|
||||
tools.prop(self, "balloon_profile_algorithm")
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -242,24 +242,39 @@ def cutout_fine_outline_property():
|
||||
)
|
||||
|
||||
|
||||
def cutout_mesh_detail_property():
|
||||
return bpy.props.EnumProperty(
|
||||
name="Mesh Detail",
|
||||
description="Control mesh density. Higher levels use more vertices to represent finer shapes.",
|
||||
items=(
|
||||
("LOW", "Low", "32 source image pixels between neighboring mesh points"),
|
||||
("MEDIUM", "Medium", "16 source image pixels between neighboring mesh points"),
|
||||
("HIGH", "High", "8 source image pixels between neighboring mesh points"),
|
||||
("ULTRA", "Ultra", "4 source image pixels between neighboring mesh points"),
|
||||
),
|
||||
default="LOW",
|
||||
def _cutout_length_scale(owner):
|
||||
scene = getattr(owner, "id_data", None)
|
||||
if not isinstance(scene, bpy.types.Scene):
|
||||
scene = bpy.context.scene
|
||||
return scene.unit_settings.scale_length
|
||||
|
||||
|
||||
def _get_cutout_edge_length(owner):
|
||||
return owner.get("cutout_edge_length_meters", 0.1) / _cutout_length_scale(owner)
|
||||
|
||||
|
||||
def _set_cutout_edge_length(owner, value):
|
||||
owner["cutout_edge_length_meters"] = value * _cutout_length_scale(owner)
|
||||
|
||||
|
||||
def cutout_edge_length_property(*, store_meters=False, **options):
|
||||
if store_meters:
|
||||
options.update(get=_get_cutout_edge_length, set=_set_cutout_edge_length)
|
||||
return bpy.props.FloatProperty(
|
||||
name="Edge Length",
|
||||
description="Target mesh edge length in world space",
|
||||
default=0.1,
|
||||
min=1e-6,
|
||||
unit="LENGTH",
|
||||
subtype="DISTANCE",
|
||||
**options,
|
||||
)
|
||||
|
||||
|
||||
class AnyImageSettings(bpy.types.PropertyGroup):
|
||||
cutout_gesture: cutout_gesture_property()
|
||||
cutout_alpha_threshold: cutout_alpha_threshold_property()
|
||||
cutout_mesh_detail: cutout_mesh_detail_property()
|
||||
cutout_edge_length: cutout_edge_length_property(store_meters=True)
|
||||
cutout_fine_outline: cutout_fine_outline_property()
|
||||
cutout_generate_normal: cutout_generate_normal_property()
|
||||
mask_gesture: mask_gesture_property()
|
||||
|
||||
Reference in New Issue
Block a user