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:
@@ -92,7 +92,7 @@ def test_cutout_material_uses_ior_1_2():
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
|
||||
result = _create_cutout_shape(source, "FLAT", 8)
|
||||
result = _create_cutout_shape(source, "FLAT", 0.1)
|
||||
|
||||
shader = next(
|
||||
node
|
||||
|
||||
132
tests/anyimage/operators/test_cutout_edge_length.py
Normal file
132
tests/anyimage/operators/test_cutout_edge_length.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
from mathutils import Matrix
|
||||
|
||||
from anyimage import properties
|
||||
from anyimage.operators.cutout_tool import geometry as g
|
||||
from anyimage.operators.cutout_tool import operators
|
||||
|
||||
|
||||
def source_plane(size=(100, 100), display_size=1, matrix=None):
|
||||
return SimpleNamespace(
|
||||
data=SimpleNamespace(size=size),
|
||||
empty_display_size=display_size,
|
||||
empty_image_offset=(-0.5, -0.5),
|
||||
matrix_world=Matrix.Identity(4) if matrix is None else matrix,
|
||||
)
|
||||
|
||||
|
||||
def test_relative_floor_uses_visible_world_bounds(monkeypatch):
|
||||
monkeypatch.setattr(g, "configured_min_cutout_relative_edge_length", lambda _: 0.01)
|
||||
alpha = np.zeros((100, 100))
|
||||
alpha[25:75, 25:75] = 1
|
||||
source = source_plane(display_size=2)
|
||||
resolve = lambda length: g.effective_cutout_edge_length(None, source, length, alpha, (0, 0, 100, 100))
|
||||
assert resolve(0.001) == 0.01
|
||||
assert resolve(0.1) == 0.1
|
||||
source.matrix_world = Matrix.Rotation(0.7, 4, "Y") @ Matrix.Diagonal((2, 3, 1, 1))
|
||||
assert resolve(0.001) == pytest.approx(0.03)
|
||||
monkeypatch.setattr(g, "configured_min_cutout_relative_edge_length", lambda _: 0.02)
|
||||
assert resolve(0.001) == pytest.approx(0.06)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fine_outline", (False, True))
|
||||
def test_mesh_density_survives_resolution_and_canvas_changes(fine_outline):
|
||||
meshes = []
|
||||
for size, display_size, inset in ((100, 1, 0), (200, 1, 0), (200, 2, 50)):
|
||||
source = source_plane((size, size), display_size)
|
||||
alpha = np.zeros((size, size))
|
||||
alpha[inset:size-inset, inset:size-inset] = 1
|
||||
spacing = g.effective_cutout_edge_length(bpy.context, source, 0.1, alpha, (0, 0, size, size))
|
||||
assert spacing == 0.1
|
||||
meshes.append(g.build_base_shape(bpy.context, source, spacing, alpha, (0, 0, size, size), fine_outline=fine_outline))
|
||||
counts = [len(mesh.vertices) for mesh in meshes]
|
||||
assert max(counts) / min(counts) < 1.05
|
||||
for mesh in meshes:
|
||||
assert np.isfinite(mesh.vertices).all()
|
||||
np.testing.assert_allclose(np.ptp(mesh.vertices[:, :2], axis=0), (1, 1), atol=0.03)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fine_outline", (False, True))
|
||||
def test_world_scaling_adds_samples_and_rotation_preserves_density(fine_outline):
|
||||
alpha = np.ones((100, 100))
|
||||
counts = []
|
||||
for matrix in (Matrix.Identity(4), Matrix.Diagonal((2, 3, 1, 1)),
|
||||
Matrix.Rotation(0.6, 4, "Z") @ Matrix.Diagonal((2, 3, 1, 1))):
|
||||
source = source_plane(matrix=matrix)
|
||||
mesh = g.build_base_shape(bpy.context, source, 0.1, alpha, (0, 0, 100, 100), fine_outline=fine_outline)
|
||||
counts.append(len(mesh.vertices))
|
||||
planar = mesh.vertices[:, :2] @ g.metric_transform(g.world_plane_metric(matrix)[0])
|
||||
edges = np.unique(np.sort(np.concatenate([mesh.faces[:, [0, 1]], mesh.faces[:, [1, 2]], mesh.faces[:, [2, 0]]]), axis=1), axis=0)
|
||||
lengths = np.linalg.norm(planar[edges[:, 0]] - planar[edges[:, 1]], axis=1)
|
||||
assert 0.06 < np.median(lengths) < 0.14
|
||||
assert counts[1] > counts[0] * 4
|
||||
assert abs(counts[2] - counts[1]) < counts[1] * 0.02
|
||||
|
||||
|
||||
def test_length_property_uses_scene_units_and_preserves_physical_value():
|
||||
class CutoutLengthTestSettings(bpy.types.PropertyGroup):
|
||||
edge_length: properties.cutout_edge_length_property(store_meters=True)
|
||||
|
||||
scene = bpy.context.scene
|
||||
previous_scale, previous_system = scene.unit_settings.scale_length, scene.unit_settings.system
|
||||
bpy.utils.register_class(CutoutLengthTestSettings)
|
||||
bpy.types.Scene.cutout_length_test = bpy.props.PointerProperty(type=CutoutLengthTestSettings)
|
||||
try:
|
||||
scene.unit_settings.system = "METRIC"
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
settings = scene.cutout_length_test
|
||||
assert settings.edge_length == pytest.approx(10)
|
||||
settings.edge_length = bpy.utils.units.to_value("METRIC", "LENGTH", "0.2m") / scene.unit_settings.scale_length
|
||||
assert settings.edge_length == pytest.approx(20)
|
||||
scene.unit_settings.scale_length = 1
|
||||
assert settings.edge_length == pytest.approx(0.2)
|
||||
finally:
|
||||
scene.unit_settings.scale_length = previous_scale
|
||||
scene.unit_settings.system = previous_system
|
||||
if "cutout_length_test" in scene:
|
||||
del scene["cutout_length_test"]
|
||||
del bpy.types.Scene.cutout_length_test
|
||||
bpy.utils.unregister_class(CutoutLengthTestSettings)
|
||||
|
||||
|
||||
def test_operator_length_property_accepts_world_units():
|
||||
captured = []
|
||||
|
||||
class CutoutLengthTestOperator(bpy.types.Operator):
|
||||
bl_idname = "anyimage.test_cutout_length"
|
||||
bl_label = "Test Cutout Length"
|
||||
edge_length: properties.cutout_edge_length_property()
|
||||
|
||||
def execute(self, context):
|
||||
captured.append(self.edge_length)
|
||||
return {"FINISHED"}
|
||||
|
||||
units = bpy.context.scene.unit_settings
|
||||
previous = units.scale_length
|
||||
bpy.utils.register_class(CutoutLengthTestOperator)
|
||||
try:
|
||||
units.scale_length = 0.01
|
||||
bpy.ops.anyimage.test_cutout_length(edge_length=20)
|
||||
assert captured == pytest.approx([20])
|
||||
finally:
|
||||
units.scale_length = previous
|
||||
bpy.utils.unregister_class(CutoutLengthTestOperator)
|
||||
|
||||
|
||||
def test_effective_length_report_and_geometry_share_value():
|
||||
report = Mock()
|
||||
context = SimpleNamespace(scene=SimpleNamespace(unit_settings=SimpleNamespace(system="METRIC", scale_length=0.01)))
|
||||
with patch.object(operators, "effective_cutout_edge_length", return_value=20), \
|
||||
patch.object(operators, "build_base_shape") as build, \
|
||||
patch.object(operators, "create_shape_object") as create, \
|
||||
patch.object(operators.bpy.ops.ed, "undo_push", return_value={"FINISHED"}):
|
||||
operators.create_cutout_shape(context, object(), "FLAT", 10, np.ones((10, 10)), (0, 0, 10, 10), color_image=object(), report=report)
|
||||
assert build.call_args.args[2] == 20
|
||||
assert create.call_args.args[3] == 20
|
||||
assert report.call_args.args[0] == {"INFO"}
|
||||
assert "20 cm" in report.call_args.args[1]
|
||||
@@ -156,13 +156,13 @@ def test_closed_mode_uses_original_geometry_without_fine_import(monkeypatch):
|
||||
)
|
||||
alpha = np.ones((60, 80))
|
||||
actual = g.build_base_shape(
|
||||
None, source, 16, alpha, (0, 0, 80, 60), fine_outline=False
|
||||
None, source, 0.4, alpha, (0, 0, 80, 60), fine_outline=False
|
||||
)
|
||||
contours = g.extract_cutout_contours(alpha, 16, threshold=0.9)
|
||||
contours = g.extract_cutout_contours(alpha, 5, threshold=0.9)
|
||||
expected = g.cutout_domain(
|
||||
contours,
|
||||
image_empty_bounds(source),
|
||||
g.cutout_sampling_spacing(source, 16),
|
||||
0.4,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
)
|
||||
np.testing.assert_array_equal(actual.vertices, expected[0])
|
||||
@@ -195,7 +195,7 @@ def test_relaxation_reduces_sampling_error_without_adding_points():
|
||||
contours = [np.asarray(((4.0, 4.0), (194.0, 4.0), (194.0, 104.0), (4.0, 104.0)))]
|
||||
spine = np.empty((0, 2))
|
||||
original = g.uniform_interior_points_for_contours(contours, 16)
|
||||
relaxed = fine.relax_points(contours, spine, (110, 200), 16)
|
||||
relaxed = fine.relax_points(contours, spine, 16)
|
||||
yy, xx = np.mgrid[6:104:4, 6:194:4]
|
||||
samples = np.column_stack((xx.ravel() + 0.5, yy.ravel() + 0.5))
|
||||
|
||||
|
||||
@@ -33,12 +33,6 @@ def mesh_component_count(faces):
|
||||
return count
|
||||
|
||||
|
||||
def test_cutout_mesh_limit_caps_actual_sampling_resolution():
|
||||
assert cutout_geometry.limit_cutout_point_spacing(32, (4096, 1024), 1024) == 32
|
||||
assert cutout_geometry.limit_cutout_point_spacing(32, (4096, 1024), 64) == 64
|
||||
assert cutout_geometry.limit_cutout_point_spacing(4, (8192, 4096), 1024) == 8
|
||||
|
||||
|
||||
def test_cutout_node_groups_are_loaded_from_the_asset():
|
||||
loaded = object()
|
||||
with patch.object(
|
||||
@@ -147,7 +141,7 @@ def test_blender_cutout_alpha_pipeline_does_not_import_pillow():
|
||||
assert np.any((antialiased > 0.0) & (antialiased < 1.0))
|
||||
|
||||
|
||||
def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
def test_blender_cutout_contour_resolution_tracks_sampling_resolution():
|
||||
expected = SelectionContours(
|
||||
components=(((((0, 0), (1, 0), (0, 1))),),),
|
||||
image_size=(1, 1),
|
||||
@@ -155,53 +149,12 @@ def test_blender_cutout_contour_resolution_tracks_mesh_detail():
|
||||
values = SimpleNamespace(shape=(1024, 1024))
|
||||
with patch.object(cutout_geometry, "extract_selection_contours", return_value=expected) as trace:
|
||||
assert cutout_geometry.extract_cutout_contours(values, 32) == expected
|
||||
assert cutout_geometry.extract_cutout_contours(values, 4) == expected
|
||||
assert cutout_geometry.extract_cutout_contours(values, 256) == expected
|
||||
|
||||
assert trace.call_args_list[0].kwargs["max_resolution"] == 512
|
||||
assert trace.call_args_list[1].kwargs["max_resolution"] == 1024
|
||||
|
||||
|
||||
def test_cutout_sampling_spacing_uses_the_complete_source_plane():
|
||||
source = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(400, 200)),
|
||||
matrix_world=object(),
|
||||
)
|
||||
with (
|
||||
patch.object(cutout_geometry, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
|
||||
patch.object(
|
||||
cutout_geometry,
|
||||
"world_plane_metric",
|
||||
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
|
||||
),
|
||||
):
|
||||
spacing = cutout_geometry.cutout_sampling_spacing(source, 32)
|
||||
|
||||
assert spacing == 0.32
|
||||
|
||||
|
||||
def test_cutout_sampling_spacing_tracks_upscaled_source_pixels():
|
||||
original = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(2048, 1024)),
|
||||
matrix_world=object(),
|
||||
)
|
||||
upscaled = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(8192, 4096)),
|
||||
matrix_world=object(),
|
||||
)
|
||||
with (
|
||||
patch.object(cutout_geometry, "image_empty_bounds", return_value=(-2.0, 2.0, -1.0, 1.0)),
|
||||
patch.object(
|
||||
cutout_geometry,
|
||||
"world_plane_metric",
|
||||
return_value=(((1.0, 0.0), (0.0, 1.0)), 1.0),
|
||||
),
|
||||
):
|
||||
original_spacing = cutout_geometry.cutout_sampling_spacing(original, 32)
|
||||
upscaled_spacing = cutout_geometry.cutout_sampling_spacing(upscaled, 32)
|
||||
|
||||
assert upscaled_spacing == original_spacing / 4
|
||||
|
||||
|
||||
def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
|
||||
assert np.isclose(
|
||||
cutout_geometry.cutout_cleanup_value(
|
||||
|
||||
@@ -35,13 +35,13 @@ class WindowManager:
|
||||
|
||||
def cutout_context(
|
||||
window_manager,
|
||||
mesh_detail="LOW",
|
||||
edge_length=0.1,
|
||||
generate_normal=False,
|
||||
alpha_threshold=0.9,
|
||||
fine_outline=True,
|
||||
):
|
||||
settings = SimpleNamespace(
|
||||
cutout_mesh_detail=mesh_detail,
|
||||
cutout_edge_length=edge_length,
|
||||
cutout_generate_normal=generate_normal,
|
||||
cutout_alpha_threshold=alpha_threshold,
|
||||
cutout_fine_outline=fine_outline,
|
||||
@@ -97,14 +97,14 @@ def test_cutout_pie_uses_two_local_shapes_without_ai(monkeypatch):
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert not any(call[2].generate_normal for call in window_manager.pie.calls)
|
||||
assert all(call[2].point_spacing == 32 for call in window_manager.pie.calls)
|
||||
assert all(call[2].edge_length == 0.1 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
window_manager = WindowManager()
|
||||
context = cutout_context(
|
||||
window_manager,
|
||||
mesh_detail="MEDIUM",
|
||||
edge_length=0.05,
|
||||
generate_normal=True,
|
||||
alpha_threshold=0.1,
|
||||
fine_outline=False,
|
||||
@@ -150,7 +150,7 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
== ((0.0, 0.0), (10.0, 0.0), (0.0, 10.0))
|
||||
for call in window_manager.pie.calls
|
||||
)
|
||||
assert all(call[2].point_spacing == 16 for call in window_manager.pie.calls)
|
||||
assert all(call[2].edge_length == 0.05 for call in window_manager.pie.calls)
|
||||
|
||||
|
||||
def test_cutout_normal_space_is_derived_from_shape():
|
||||
@@ -162,13 +162,6 @@ def test_cutout_normal_space_is_derived_from_shape():
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SOLID", True) == "OBJECT"
|
||||
|
||||
|
||||
def test_cutout_mesh_detail_maps_to_denser_sampling():
|
||||
assert [
|
||||
interaction.cutout_mesh_detail_spacing(detail)
|
||||
for detail in ("LOW", "MEDIUM", "HIGH", "ULTRA")
|
||||
] == [32, 16, 8, 4]
|
||||
|
||||
|
||||
def test_cutout_lasso_submission_passes_only_the_selected_path(monkeypatch):
|
||||
source = cutout_source()
|
||||
window_manager = WindowManager()
|
||||
|
||||
@@ -43,13 +43,13 @@ def test_cutout_depth_axis_is_adjusted_only_in_modifier(shape, gesture):
|
||||
positive_x = _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
8,
|
||||
0.1,
|
||||
gesture=gesture,
|
||||
)
|
||||
negative_z = _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
8,
|
||||
0.1,
|
||||
gesture=gesture,
|
||||
)
|
||||
|
||||
@@ -160,7 +160,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"SOLID",
|
||||
8,
|
||||
0.1,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
|
||||
@@ -211,7 +211,7 @@ def test_teddy_preference_writes_the_shared_balloon_attribute():
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"FLAT",
|
||||
8,
|
||||
0.1,
|
||||
)
|
||||
|
||||
attribute = result.data.attributes[cutout_shape.BALLOON_ATTRIBUTE_NAME]
|
||||
@@ -251,7 +251,7 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_SOLID",
|
||||
8,
|
||||
0.25,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
gesture=gesture,
|
||||
@@ -270,7 +270,7 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
0.0,
|
||||
)
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_sampling_spacing(source, 8),
|
||||
0.25,
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
@@ -374,7 +374,7 @@ def test_depth_surface_keeps_continuous_points_across_zero_validity_edge():
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_SOLID",
|
||||
8,
|
||||
0.1,
|
||||
depth_image=_depth_image(points, validity),
|
||||
depth_metadata=_depth_metadata(intrinsics, (size, size), 1.0),
|
||||
)
|
||||
@@ -621,7 +621,7 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
result = _create_cutout_shape(
|
||||
source,
|
||||
"DEPTH_BALLOON",
|
||||
8,
|
||||
0.1,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
bounds=crop_bounds,
|
||||
@@ -869,7 +869,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
bpy.context,
|
||||
source,
|
||||
"FLAT",
|
||||
16,
|
||||
0.1,
|
||||
content_values,
|
||||
selection_mask.bounds,
|
||||
alpha_threshold=0.5,
|
||||
@@ -885,7 +885,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES["FLAT"]
|
||||
expected_cleanup = cutout_geometry.cutout_cleanup_value(
|
||||
cutout_geometry.cutout_sampling_spacing(source, 16),
|
||||
0.1,
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
|
||||
@@ -69,7 +69,7 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
),
|
||||
shape=shape,
|
||||
gesture="POLYLINE",
|
||||
point_spacing=32,
|
||||
edge_length=0.1,
|
||||
alpha_threshold=0.25,
|
||||
fine_outline=False,
|
||||
generate_normal=False,
|
||||
@@ -98,11 +98,6 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
"configured_max_ai_input_size",
|
||||
return_value=2048,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
"configured_max_cutout_mesh_resolution",
|
||||
return_value=1024,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
"_cutout_content_values",
|
||||
@@ -128,13 +123,14 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
context,
|
||||
source,
|
||||
shape,
|
||||
32.0,
|
||||
0.1,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
alpha_threshold=0.25,
|
||||
fine_outline=False,
|
||||
color_image="Color",
|
||||
gesture="POLYLINE",
|
||||
report=operator.report,
|
||||
)
|
||||
|
||||
|
||||
@@ -161,7 +157,8 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
{"points": [[1, 1], [7, 1], [7, 3], [1, 3]]}
|
||||
)
|
||||
operator.shape = "DEPTH_SOLID"
|
||||
operator.point_spacing = 32
|
||||
operator.report = lambda *_args: None
|
||||
operator.edge_length = 0.1
|
||||
operator.generate_normal = False
|
||||
operator.report = lambda *_args: None
|
||||
|
||||
@@ -187,11 +184,6 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
"configured_max_ai_input_size",
|
||||
return_value=2048,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
"configured_max_cutout_mesh_resolution",
|
||||
return_value=1024,
|
||||
),
|
||||
patch.object(
|
||||
self.cutout_main,
|
||||
"invoke_ai_setup_if_needed",
|
||||
@@ -241,7 +233,7 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
{"points": [[1, 1], [7, 1], [7, 3], [1, 3]]}
|
||||
),
|
||||
shape="FLAT",
|
||||
point_spacing=32,
|
||||
edge_length=0.1,
|
||||
generate_normal=False,
|
||||
report=lambda level, message: reports.append((level, message)),
|
||||
)
|
||||
@@ -377,7 +369,7 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
layout.events,
|
||||
[
|
||||
("prop", "cutout_gesture", True),
|
||||
("prop", "cutout_mesh_detail", True),
|
||||
("prop", "cutout_edge_length", True),
|
||||
("prop", "cutout_fine_outline", True),
|
||||
("prop", "cutout_alpha_threshold", True),
|
||||
setup,
|
||||
@@ -413,7 +405,8 @@ class CutoutOperatorsTest(BlenderTestCase):
|
||||
operator.color_path = __file__
|
||||
operator.shape = "FLAT"
|
||||
operator.gesture = "POLYLINE"
|
||||
operator.point_spacing = 32
|
||||
operator.report = lambda *_args: None
|
||||
operator.edge_length = 0.1
|
||||
operator.generate_normal = True
|
||||
operator._selection_mask = selection_mask
|
||||
result = SimpleNamespace(value={}, file=lambda _key: Path(__file__))
|
||||
|
||||
@@ -178,7 +178,7 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
drawn_settings,
|
||||
[
|
||||
"cutout_gesture",
|
||||
"cutout_mesh_detail",
|
||||
"cutout_edge_length",
|
||||
"cutout_fine_outline",
|
||||
"cutout_alpha_threshold",
|
||||
"cutout_generate_normal",
|
||||
@@ -226,7 +226,7 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
"radius",
|
||||
},
|
||||
)
|
||||
self.assertEqual(settings["cutout_mesh_detail"]["default"], "LOW")
|
||||
self.assertEqual(settings["cutout_edge_length"]["default"], 0.1)
|
||||
self.assertFalse(settings["cutout_fine_outline"]["default"])
|
||||
self.assertFalse(cutout_module.CutoutSelectionToShape.__annotations__["fine_outline"]["default"])
|
||||
self.assertEqual(settings["cutout_alpha_threshold"]["default"], 0.9)
|
||||
@@ -235,18 +235,10 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
self.assertEqual(settings["cutout_alpha_threshold"]["soft_min"], 0.1)
|
||||
self.assertEqual(settings["cutout_alpha_threshold"]["soft_max"], 0.9)
|
||||
self.assertEqual(settings["cutout_alpha_threshold"]["subtype"], "FACTOR")
|
||||
self.assertEqual(
|
||||
[item[:2] for item in settings["cutout_mesh_detail"]["items"]],
|
||||
[
|
||||
("LOW", "Low"),
|
||||
("MEDIUM", "Medium"),
|
||||
("HIGH", "High"),
|
||||
("ULTRA", "Ultra"),
|
||||
],
|
||||
)
|
||||
self.assertEqual(settings["cutout_edge_length"]["unit"], "LENGTH")
|
||||
self.assertFalse(settings["cutout_generate_normal"]["default"])
|
||||
self.assertEqual(
|
||||
settings["cutout_mesh_detail"]["name"], "Mesh Detail"
|
||||
settings["cutout_edge_length"]["name"], "Edge Length"
|
||||
)
|
||||
self.assertEqual(
|
||||
settings["cutout_generate_normal"]["name"],
|
||||
@@ -287,7 +279,7 @@ class AddonRegistrationTest(BlenderTestCase):
|
||||
|
||||
settings = self.anyimage.properties.AnyImageSettings.__annotations__
|
||||
self.assertEqual(set(settings), {
|
||||
"cutout_gesture", "cutout_alpha_threshold", "cutout_mesh_detail", "cutout_fine_outline",
|
||||
"cutout_gesture", "cutout_alpha_threshold", "cutout_edge_length", "cutout_fine_outline",
|
||||
"cutout_generate_normal", "mask_gesture",
|
||||
"mask_mode", "mask_radius",
|
||||
})
|
||||
|
||||
@@ -132,24 +132,16 @@ class PreferencesTest(BlenderTestCase):
|
||||
)
|
||||
|
||||
|
||||
def test_cutout_mesh_resolution_defaults_and_clamps_independently(self):
|
||||
preference_module = self.anyimage.preferences
|
||||
def test_cutout_relative_edge_length_defaults_and_clamps(self):
|
||||
module = self.anyimage.preferences
|
||||
preferences = self.fake_bpy.context.preferences.addons["anyimage"].preferences
|
||||
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 2048
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
1024,
|
||||
)
|
||||
preferences.max_cutout_mesh_resolution = 1
|
||||
self.assertEqual(
|
||||
preference_module.configured_max_cutout_mesh_resolution(),
|
||||
4,
|
||||
)
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.01)
|
||||
preferences.min_cutout_relative_edge_length = 2
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 0.02)
|
||||
preferences.min_cutout_relative_edge_length = 200
|
||||
self.assertEqual(module.configured_min_cutout_relative_edge_length(), 1)
|
||||
preferences.min_cutout_relative_edge_length = 0
|
||||
self.assertAlmostEqual(module.configured_min_cutout_relative_edge_length(), 0.00001)
|
||||
|
||||
|
||||
def test_frame_resolution_defaults_and_clamps_independently(self):
|
||||
|
||||
@@ -29,7 +29,7 @@ def _group_node(group, node_tree):
|
||||
def _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
edge_length,
|
||||
*,
|
||||
bounds=None,
|
||||
depth_image=None,
|
||||
@@ -52,7 +52,7 @@ def _create_cutout_shape(
|
||||
base_shape = cutout_geometry.build_base_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
detail,
|
||||
edge_length,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
)
|
||||
@@ -60,7 +60,7 @@ def _create_cutout_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
edge_length,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
base_shape,
|
||||
|
||||
@@ -25,8 +25,8 @@ def set_test_structure(monkeypatch, value):
|
||||
from tools.nodes.groups import image_depth_cutout
|
||||
build = image_depth_cutout.build_front_fields
|
||||
|
||||
def fields(group, rim, normal, normal_weight):
|
||||
result = build(group, rim, normal, normal_weight)
|
||||
def fields(group, volume, normal, normal_weight):
|
||||
result = build(group, volume, normal, normal_weight)
|
||||
structure = group.nodes.new("ShaderNodeValue")
|
||||
structure.outputs[0].default_value = value
|
||||
result["structure"] = structure.outputs[0]
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
assert len(thickness) == 2 and len(set(thickness)) == 2
|
||||
if group.name == "O Image Depth Cutout":
|
||||
inflation = next(item for item in sockets if item.name == "Front Inflation")
|
||||
assert inflation.default_value == 0.5
|
||||
assert inflation.default_value == 0.0
|
||||
result[group.name] = [
|
||||
(item.identifier, item.name, item.description) for item in sockets
|
||||
]
|
||||
@@ -82,6 +82,35 @@ def test_saved_parameter_descriptions_match_definitions():
|
||||
assert descriptions(target.node_groups) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["definition", "asset"])
|
||||
def test_geometry_nodes_contribute_to_output(source):
|
||||
from tools.nodes.build import build_node_groups
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
if source == "definition":
|
||||
groups = build_node_groups()
|
||||
else:
|
||||
with bpy.data.libraries.load(str(node_asset_path())) as (library, target):
|
||||
target.node_groups = list(library.node_groups)
|
||||
groups = target.node_groups
|
||||
for group in groups:
|
||||
if group.bl_idname != "GeometryNodeTree":
|
||||
continue
|
||||
pending = [node for node in group.nodes if node.bl_idname == "NodeGroupOutput"]
|
||||
used = set()
|
||||
while pending:
|
||||
node = pending.pop()
|
||||
if node in used:
|
||||
continue
|
||||
used.add(node)
|
||||
pending.extend(link.from_node for socket in node.inputs for link in socket.links)
|
||||
pending.extend(candidate for candidate in group.nodes
|
||||
if getattr(candidate, "paired_output", None) == node)
|
||||
unused = [node.name for node in group.nodes
|
||||
if node not in used and node.bl_idname != "NodeFrame"]
|
||||
assert not unused, (group.name, unused)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["definition", "asset"])
|
||||
def test_geometry_attribute_contract_includes_nested_groups(source):
|
||||
from tools.nodes.build import build_node_groups
|
||||
|
||||
Reference in New Issue
Block a user