Add comprehensive tests for cutout and mesh functionalities

- Introduced `test_cutout.py` to validate cutout pie menu behavior, image lasso options, and mesh detail calculations.
- Added `test_mesh.py` to ensure correct tangent normal rasterization, plane source topology, and depth surface evaluations.
- Created `test_mesh_blender.py` to verify material node groups, tangent baking, and local cutout building from Blender images.
- Implemented tests for handling alpha regions and ensuring geometry nodes maintain expected behavior.
This commit is contained in:
2026-08-26 10:42:29 +08:00
parent 0cf04a13c6
commit 2b79a342b0
46 changed files with 5554 additions and 645 deletions

View File

@@ -3,25 +3,31 @@ import math
import bpy
from .depth import (
add_balloon_shape_modifier,
assign_balloon_shape_attributes,
balloon_depth_fields,
create_balloon_base_plane,
load_balloon_depth_data,
)
from .geometry import INFLATION, POINT_SPACING, triangulate_balloon
from ...properties import (
BEN2_MODEL_KEY,
ai_ready,
ai_setup_label,
ai_status,
)
from ...properties import (
balloon_contour_analysis_property as contour_analysis_property,
)
from ...properties import (
balloon_depth_analysis_property as depth_analysis_property,
)
from ...properties import (
balloon_depth_model_property as depth_model_property,
)
from ...properties import (
balloon_mode_property as mode_property,
)
from ...properties import (
balloon_resolution_property as resolution_property,
)
from ...properties import (
balloon_reuse_material_property as reuse_material_property,
)
from ...properties import (
balloon_thickness_property as thickness_property,
)
from ...runtime import JobOperatorBase
@@ -44,12 +50,19 @@ from ..image import (
video_total_frames,
)
from ..material import (
BALLOON_MATERIAL_NODE_GROUP_NAME,
TEXTURE_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
create_material,
)
from ..normal_map import bake_tangent_normal_image
from ..surface import assign_vertex_uv
from .depth import (
add_balloon_shape_modifier,
assign_balloon_shape_attributes,
balloon_depth_fields,
create_balloon_base_plane,
load_balloon_depth_data,
)
from .geometry import INFLATION, POINT_SPACING, triangulate_balloon
MIN_PATH_DISTANCE = 2.0
BALLOON_MATERIAL_PROPERTY = "anyimage_balloon_material"
@@ -151,7 +164,6 @@ def data_name_matches(name, base_name):
return suffix != name and suffix.isdigit()
def marked_balloon_material(source_image, normal_required=False):
return next(
(
@@ -160,9 +172,7 @@ def marked_balloon_material(source_image, normal_required=False):
if callable(getattr(material, "get", None))
and bool(material.get(BALLOON_MATERIAL_PROPERTY))
and material.get(BALLOON_SOURCE_IMAGE_PROPERTY) is source_image
and (
material.get(BALLOON_NORMAL_IMAGE_PROPERTY) is not None
)
and (material.get(BALLOON_NORMAL_IMAGE_PROPERTY) is not None)
== normal_required
),
None,
@@ -227,6 +237,7 @@ BALLOON_AXIS_TO_SOURCE = (
(0.0, 0.0, 0.0, 1.0),
)
def orient_balloon_vertices(vertices):
"""Rotate legacy XY/Z Balloon coordinates into a YZ/-X frame."""
return [(-z, -x, y) for x, y, z in vertices]
@@ -237,14 +248,16 @@ def orient_balloon_world_matrix(source_matrix):
return source_matrix @ Matrix(BALLOON_AXIS_TO_SOURCE)
def orient_balloon_base_plane(local_plane):
slope_x, slope_y, offset = local_plane
return (slope_x, -slope_y, -offset)
def extract_balloon_object(
context,
source_object,
polygon,
regions,
matrix_world,
algorithm,
thickness=1.0,
@@ -253,8 +266,10 @@ def extract_balloon_object(
reuse_material=True,
depth_data=None,
normal_path=None,
material_image=None,
bake_normal_to_tangent=False,
):
image = source_object.data
image = material_image or source_object.data
image_size = tuple(image.size)
local_bounds = plane_bounds(source_object)
world_metric, world_z_scale = world_plane_metric(matrix_world)
@@ -263,7 +278,7 @@ def extract_balloon_object(
resolution,
)
vertices, faces, vertex_uv = triangulate_balloon(
polygon,
regions,
image_size,
local_bounds,
spacing=point_spacing,
@@ -315,7 +330,9 @@ def extract_balloon_object(
finally:
bpy.data.images.remove(update_normal)
if material is None:
color_image = reusable_color_image(color_name) if reuse_material else None
color_image = material_image
if color_image is None and reuse_material:
color_image = reusable_color_image(color_name)
if color_image is None:
color_image = image.copy()
color_image.name = color_name
@@ -330,12 +347,8 @@ def extract_balloon_object(
material = create_material(
material_name,
color_image,
1.0,
surface_group_name=(
BALLOON_MATERIAL_NODE_GROUP_NAME
if normal_image is not None
else TEXTURE_MATERIAL_NODE_GROUP_NAME
),
0.0,
surface_group_name=IMAGE_MATERIAL_NODE_GROUP_NAME,
normal_image=normal_image,
scene=context.scene,
)
@@ -369,7 +382,18 @@ def extract_balloon_object(
vertices,
local_base_plane,
)
add_balloon_shape_modifier(balloon, thickness, base_plane)
add_balloon_shape_modifier(
balloon,
thickness,
base_plane,
)
if bake_normal_to_tangent and normal_image is not None:
bake_tangent_normal_image(
context,
source_object,
balloon,
normal_image,
)
if depth_data is not None:
balloon["anyimage_balloon_depth_analysis"] = True
balloon["anyimage_reference_depth"] = depth_data["reference_depth"]
@@ -625,7 +649,7 @@ class ExtractBalloon(bpy.types.Operator):
balloon = extract_balloon_object(
context,
source_object,
polygon,
((polygon,),),
matrix_world,
self.mode,
thickness=self.thickness,
@@ -710,7 +734,7 @@ class AnalyzeBalloon(JobOperatorBase, bpy.types.Operator):
balloon = extract_balloon_object(
context,
source_object,
polygon,
((polygon,),),
matrix_world,
self.mode,
thickness=self.thickness,
@@ -719,6 +743,7 @@ class AnalyzeBalloon(JobOperatorBase, bpy.types.Operator):
reuse_material=self.reuse_material,
depth_data=depth_data,
normal_path=(result.file("normal") if self.depth_analysis else None),
bake_normal_to_tangent=self.depth_analysis,
)
balloon["anyimage_resolution_m"] = self.resolution
if self.contour_analysis: