feat: Implement depth cutout symmetry and associated functionalities
- Introduced a new proposal for depth cutout symmetry, allowing for the reuse of depth replacement and geometry operations. - Added requirements for maintaining default thickness values for cutout nodes and ensuring proper initialization during object creation. - Enhanced symmetry node to handle existing geometry, including alignment, mirroring, and boundary filling. - Implemented fill smoothing to control the geometry of generated walls while preserving original surfaces. - Created tests to validate the functionality of the new symmetry features against baseline geometry. - Updated existing assets and node groups to support the new symmetry capabilities without introducing additional dependencies.
This commit is contained in:
@@ -10,7 +10,7 @@ from anyimage.operators.cutout_tool.balloon import poisson_balloon_profile
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_depth_presets_always_generate_the_correct_normal_space(enabled):
|
||||
assert normal_mode_for_shape("DEPTH_SOLID", enabled) == "OBJECT"
|
||||
assert normal_mode_for_shape("DEPTH_SYMMETRY", enabled) == "TANGENT"
|
||||
assert normal_mode_for_shape("DEPTH_SYMMETRY", enabled) == "OBJECT"
|
||||
for shape in ("FLAT", "SOLID"):
|
||||
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
|
||||
|
||||
|
||||
@@ -155,10 +155,10 @@ def test_cutout_pie_passes_image_lasso_options_to_all_ai_shapes(monkeypatch):
|
||||
|
||||
def test_cutout_normal_space_is_derived_from_shape():
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SOLID", False) == "OBJECT"
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SYMMETRY", False) == "TANGENT"
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SYMMETRY", False) == "OBJECT"
|
||||
assert cutout_main.normal_mode_for_shape("FLAT", True) == "TANGENT"
|
||||
assert cutout_main.normal_mode_for_shape("SOLID", True) == "TANGENT"
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SYMMETRY", True) == "TANGENT"
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SYMMETRY", True) == "OBJECT"
|
||||
assert cutout_main.normal_mode_for_shape("DEPTH_SOLID", True) == "OBJECT"
|
||||
|
||||
|
||||
|
||||
@@ -250,8 +250,10 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES["DEPTH_SOLID"]
|
||||
layer = _group_node(material_data.material_node_group(), result.data.materials[0].node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is True
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Mode")) == (1 if gesture == "POLYLINE" else 0)
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE")) == 0.5
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE")) == pytest.approx(0.2)
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")) == 1.0
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
@@ -623,134 +625,47 @@ def test_depth_symmetry_uses_metric_plane_inputs(gesture):
|
||||
source.name,
|
||||
result.name,
|
||||
}
|
||||
modifier = result.modifiers[0]
|
||||
mode_id = object_data.modifier_input_identifier(modifier.node_group, "Mode")
|
||||
assert modifier_value(modifier, mode_id) == 1
|
||||
object_data.set_modifier_input(modifier, mode_id, 0)
|
||||
depth_image_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Image",
|
||||
)
|
||||
assert isinstance(modifier_value(modifier, depth_image_id), bpy.types.Image)
|
||||
expected_center = np.zeros(2, dtype=np.float64)
|
||||
crop_center = Vector(
|
||||
(
|
||||
(local_bounds[0] + local_bounds[1]) * 0.5,
|
||||
(local_bounds[2] + local_bounds[3]) * 0.5,
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
expected_origin = source.matrix_world @ crop_center
|
||||
assert np.allclose(
|
||||
tuple(result.matrix_world.translation),
|
||||
tuple(expected_origin),
|
||||
atol=1e-5,
|
||||
)
|
||||
assert len(result.modifiers) == 2
|
||||
modifier, symmetry = result.modifiers
|
||||
assert modifier.node_group.name == "O Image Depth Cutout"
|
||||
assert symmetry.node_group.name == "O Image Cutout Symmetry"
|
||||
layer = _group_node(material_data.material_node_group(), result.data.materials[0].node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is True
|
||||
assert not modifier.show_group_selector and not symmetry.show_group_selector
|
||||
|
||||
depth_reference_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Reference Depth",
|
||||
)
|
||||
assert not np.isclose(modifier_value(modifier, depth_reference_id), 8.0)
|
||||
depth_direction_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Direction",
|
||||
)
|
||||
direction = np.asarray(modifier_value(modifier, depth_direction_id), dtype=np.float64)
|
||||
assert not np.allclose(direction, (0.0, 0.0, 1.0))
|
||||
uniform_scale_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Uniform Scale",
|
||||
)
|
||||
assert modifier_value(modifier, uniform_scale_id) > 0.0
|
||||
thickness_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Thickness",
|
||||
)
|
||||
smooth_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Smooth Weight",
|
||||
)
|
||||
assert modifier_value(modifier, smooth_id) == 1.0
|
||||
object_data.set_modifier_input(modifier, smooth_id, 0.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
def value(modifier, name, subtype=None):
|
||||
return modifier_value(modifier, object_data.modifier_input_identifier(
|
||||
modifier.node_group, name, subtype=subtype))
|
||||
|
||||
assert value(modifier, "Mode") == (1 if gesture == "POLYLINE" else 0)
|
||||
for subtype in ("NONE", "DISTANCE"):
|
||||
assert value(modifier, "Thickness", subtype) == 0
|
||||
initialized = {"Mode", "Cleanup Threshold", "Depth Scale", "Uniform Scale", "Reference Depth", "Depth Image"}
|
||||
for socket in modifier.node_group.interface.items_tree:
|
||||
if (socket.item_type == "SOCKET" and socket.in_out == "INPUT"
|
||||
and socket.name not in initialized
|
||||
and socket.socket_type in {"NodeSocketFloat", "NodeSocketInt"}):
|
||||
assert modifier_value(modifier, socket.identifier) == pytest.approx(socket.default_value)
|
||||
assert value(modifier, "Depth Axis") == 0
|
||||
assert isinstance(value(modifier, "Depth Image"), bpy.types.Image)
|
||||
assert value(modifier, "Uniform Scale") > 0
|
||||
assert value(modifier, "Reference Depth") > 0
|
||||
assert not np.allclose(value(symmetry, "Symmetry Direction"), (0, 0, 1))
|
||||
crop_center = Vector(((local_bounds[0] + local_bounds[1]) / 2,
|
||||
(local_bounds[2] + local_bounds[3]) / 2, 0))
|
||||
np.testing.assert_allclose(result.matrix_world.translation, source.matrix_world @ crop_center, atol=1e-5)
|
||||
from scipy.spatial import cKDTree
|
||||
positions = _evaluated_positions(result)
|
||||
center_distance = np.linalg.norm(positions[:, :2] - expected_center, axis=1)
|
||||
nearest_center = center_distance <= center_distance.min() + 1e-6
|
||||
depth_at_thickness_one = positions[nearest_center, 2].max()
|
||||
object_data.set_modifier_input(modifier, thickness_id, 2.0)
|
||||
assert len(positions) and np.isfinite(positions).all()
|
||||
assert cKDTree(positions).query(positions * (1, 1, -1))[0].max() < 1e-5
|
||||
# Both modes start from the same front, including after changing the menu.
|
||||
object_data.set_modifier_input(modifier,
|
||||
object_data.modifier_input_identifier(modifier.node_group, "Mode"),
|
||||
0 if gesture == "POLYLINE" else 1)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(result)
|
||||
center_distance = np.linalg.norm(positions[:, :2] - expected_center, axis=1)
|
||||
nearest_center = center_distance <= center_distance.min() + 1e-6
|
||||
depth_at_thickness_two = positions[nearest_center, 2].max()
|
||||
assert np.isclose(
|
||||
depth_at_thickness_one,
|
||||
depth_at_thickness_two,
|
||||
atol=1e-5,
|
||||
)
|
||||
object_data.set_modifier_input(modifier, thickness_id, 1.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
before_positions = _evaluated_positions(result)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier_value(modifier, depth_reference_id) + 0.2,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
after_positions = _evaluated_positions(result)
|
||||
assert np.isclose(
|
||||
(after_positions[:, 2] - before_positions[:, 2]).max(),
|
||||
0.2,
|
||||
atol=1e-5,
|
||||
)
|
||||
|
||||
depth_scale_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Scale",
|
||||
)
|
||||
object_data.set_modifier_input(modifier, depth_scale_id, 0.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
zero_scale_before = _evaluated_positions(result)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier_value(modifier, depth_reference_id) + 0.15,
|
||||
)
|
||||
object_data.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
zero_scale_after = _evaluated_positions(result)
|
||||
assert np.allclose(zero_scale_after, zero_scale_before, atol=1e-5)
|
||||
object_data.set_modifier_input(modifier, depth_scale_id, 1.0)
|
||||
|
||||
double_sided_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Double Sided",
|
||||
)
|
||||
object_data.set_modifier_input(modifier, double_sided_id, True)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
mirrored = _evaluated_positions(result)
|
||||
layers = {}
|
||||
for x, y, z in mirrored:
|
||||
layers.setdefault((round(float(x), 6), round(float(y), 6)), []).append(z)
|
||||
assert layers
|
||||
for z_values in layers.values():
|
||||
if len(z_values) == 1:
|
||||
assert np.isclose(z_values[0], 0.0, atol=1e-5)
|
||||
continue
|
||||
assert np.isclose(
|
||||
min(z_values) + max(z_values),
|
||||
0.0,
|
||||
atol=1e-5,
|
||||
)
|
||||
changed = _evaluated_positions(result)
|
||||
np.testing.assert_array_equal(changed, positions)
|
||||
|
||||
|
||||
def test_bounds_input_preserves_rectangular_rgba_and_stays_packed_after_cleanup():
|
||||
|
||||
Reference in New Issue
Block a user