Add comprehensive tests for image processing and mask functionalities
- Introduced tests for color image handling, ensuring source preservation and premultiplied alpha usage. - Implemented tests for brush stroke operations, validating segment boundaries and brush anti-aliasing. - Added tests for frame anti-aliasing, confirming correct rendering behavior with various alpha values and depth settings. - Created tests for frame projection, ensuring accurate overlap calculations and half-plane clipping. - Developed tests for image premultiplied retention, verifying behavior during loading and packing processes. - Added undo functionality tests for mask operations, ensuring single transaction behavior during edits.
This commit is contained in:
@@ -28,6 +28,8 @@ from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool import main as cutout_main
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
||||
from anyimage.operators.image_edit_tool import mask as edit_mask
|
||||
from anyimage.operators.image_edit_tool import frame as edit_frame
|
||||
from anyimage.server.geometry.depth_texture import (
|
||||
write_vector_depth_texture,
|
||||
write_z_depth_texture,
|
||||
@@ -60,7 +62,7 @@ def _create_cutout_shape(
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
selection_values=None,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.POSITIVE_X,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
@@ -125,38 +127,6 @@ def _depth_metadata(intrinsics, image_size, reference_depth):
|
||||
}
|
||||
|
||||
|
||||
def test_keep_original_places_and_activates_result_copy():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Crop Source", width=8, height=4, alpha=True)
|
||||
result_image = bpy.data.images.new("Crop Result", width=4, height=2, alpha=True)
|
||||
source = bpy.data.objects.new("Crop Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = source_image
|
||||
source.empty_display_size = 3.0
|
||||
source.empty_image_offset = (0.2, -0.1)
|
||||
source.matrix_world = Matrix.Translation((1.0, 2.0, 3.0))
|
||||
bpy.context.collection.objects.link(source)
|
||||
source.select_set(True)
|
||||
bpy.context.view_layer.objects.active = source
|
||||
|
||||
placed = image_data.place_empty_image_result(
|
||||
bpy.context,
|
||||
source,
|
||||
result_image,
|
||||
placement_bounds=(2, 1, 6, 3),
|
||||
keep_original=True,
|
||||
)
|
||||
|
||||
assert placed is not source
|
||||
assert source.data == source_image
|
||||
assert placed.data == result_image
|
||||
assert placed.users_collection[:] == source.users_collection[:]
|
||||
assert np.allclose(placed.matrix_world, source.matrix_world)
|
||||
assert not source.select_get()
|
||||
assert placed.select_get()
|
||||
assert bpy.context.view_layer.objects.active == placed
|
||||
|
||||
|
||||
def test_image_edit_result_uses_complete_shared_source_name():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Poster.png", width=2, height=2, alpha=True)
|
||||
@@ -177,6 +147,9 @@ def test_image_edit_result_uses_complete_shared_source_name():
|
||||
image_data.replace_empty_image(source, result_image)
|
||||
|
||||
assert shared.data == source_image
|
||||
assert source_image.alpha_mode == "STRAIGHT"
|
||||
assert result_image.alpha_mode == "PREMUL"
|
||||
assert source.use_empty_image_alpha
|
||||
assert source.data == result_image
|
||||
assert result_image.name == "Poster.png.001"
|
||||
assert "_color" not in result_image.name
|
||||
@@ -202,6 +175,194 @@ def test_image_edit_result_recovers_unshared_source_name():
|
||||
assert bpy.data.images.get("Poster.png") == result_image
|
||||
|
||||
|
||||
def test_mask_frame_mask_add_preserves_packed_hidden_rgb():
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Frame RGB", width=4, height=4, alpha=True)
|
||||
rgba = np.ones((4, 4, 4), dtype=np.float32)
|
||||
rgba[..., :3] = np.arange(48).reshape((4, 4, 3)) / 47.0
|
||||
source_image.pixels.foreach_set(np.flipud(rgba).ravel())
|
||||
source_image.update()
|
||||
expected = image_data.image_rgba(source_image).copy()
|
||||
source = bpy.data.objects.new("Frame RGB", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.empty_display_size = 2.0
|
||||
source.empty_image_offset = (-0.5, -0.5)
|
||||
source.data = source_image
|
||||
bpy.context.collection.objects.link(source)
|
||||
selection = SelectionMask(np.ones((4, 4), dtype=np.float32), (0, 0, 4, 4))
|
||||
operator = SimpleNamespace(mode="SUBTRACT", report=lambda *_args: None)
|
||||
assert edit_mask.EditImageAlpha._apply_mask(
|
||||
operator, SimpleNamespace(), source, selection
|
||||
) == {"FINISHED"}
|
||||
descriptor = edit_frame._frame_source(
|
||||
source, SimpleNamespace(width=4, height=4),
|
||||
SimpleNamespace(perspective_matrix=Matrix.Identity(4), is_perspective=False),
|
||||
Matrix.Identity(4), active=True,
|
||||
)
|
||||
np.testing.assert_allclose(descriptor["rgba"][..., :3], expected[..., :3])
|
||||
np.testing.assert_array_equal(descriptor["pixels"], 0)
|
||||
framed = edit_frame.composite_frame_pixels(
|
||||
(descriptor,), ((0, 4), (4, 4), (4, 0), (0, 0)), (4, 4)
|
||||
)
|
||||
result = image_data.create_image_edit_result(source.data, framed, (4, 4))
|
||||
image_data.replace_empty_image(source, result)
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
assert source.use_empty_image_alpha
|
||||
assert result.packed_file is not None
|
||||
packed = np.asarray(Image.open(BytesIO(result.packed_file.data)).convert("RGBA")) / 255.0
|
||||
np.testing.assert_allclose(packed[..., :3], expected[..., :3], atol=1 / 255)
|
||||
np.testing.assert_array_equal(packed[..., 3], 0)
|
||||
result.reload()
|
||||
assert result.alpha_mode == "PREMUL"
|
||||
operator.mode = "ADD"
|
||||
assert edit_mask.EditImageAlpha._apply_mask(
|
||||
operator, SimpleNamespace(), source, selection
|
||||
) == {"FINISHED"}
|
||||
restored = image_data.image_rgba(source.data)
|
||||
np.testing.assert_allclose(restored[..., :3], expected[..., :3], atol=1 / 255)
|
||||
np.testing.assert_array_equal(restored[..., 3], 1)
|
||||
|
||||
|
||||
def test_frame_restored_alpha_keeps_foreground_blended_into_white():
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
sources = []
|
||||
for name, color, depth in (("Background", (1, 1, 1, 0), 2),
|
||||
("Foreground", (0.2, 0.4, 0.6, 1), 1)):
|
||||
image = bpy.data.images.new(name, width=6, height=2, alpha=True)
|
||||
rgba = np.tile(color, (2, 6, 1)).astype(np.float32)
|
||||
if name == "Foreground":
|
||||
rgba[..., 3] = (0, 0.01, 0.1, 0.25, 0.5, 1)
|
||||
rgba[:, 0, :3] = 0
|
||||
image.pixels.foreach_set(np.flipud(rgba).ravel())
|
||||
image.update()
|
||||
source = bpy.data.objects.new(name, None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.empty_display_size = 2
|
||||
source.empty_image_offset = (-0.5, -0.5)
|
||||
source.data = image
|
||||
source.matrix_world = Matrix.Translation((0, 0, -depth))
|
||||
bpy.context.collection.objects.link(source)
|
||||
sources.append(edit_frame._frame_source(
|
||||
source, SimpleNamespace(width=6, height=2),
|
||||
SimpleNamespace(perspective_matrix=Matrix.Identity(4), is_perspective=False),
|
||||
Matrix.Identity(4), active=name == "Foreground",
|
||||
))
|
||||
front = sources[1]
|
||||
source_alpha = front["rgba"][..., 3:4]
|
||||
expected = front["rgba"][..., :3] * source_alpha + 1 - source_alpha
|
||||
# The image aspect makes its orthographic screen projection two thirds of a pixel high.
|
||||
pixels = edit_frame.composite_frame_pixels(
|
||||
sources, ((0, 4 / 3), (6, 4 / 3), (6, 2 / 3), (0, 2 / 3)), (6, 2)
|
||||
)
|
||||
result = image_data.create_image_edit_result(front["object"].data, pixels, (6, 2))
|
||||
image_data.replace_empty_image(front["object"], result)
|
||||
encoded = np.asarray(Image.open(BytesIO(result.packed_file.data)).convert("RGBA")) / 255
|
||||
np.testing.assert_allclose(encoded[..., :3], expected, atol=1 / 255)
|
||||
np.testing.assert_allclose(encoded[..., 3:4], source_alpha, atol=1 / 255)
|
||||
selection = SelectionMask(np.ones((2, 6), dtype=np.float32), (0, 0, 6, 2))
|
||||
operator = SimpleNamespace(mode="ADD", report=lambda *_args: None)
|
||||
assert edit_mask.EditImageAlpha._apply_mask(
|
||||
operator, SimpleNamespace(), front["object"], selection
|
||||
) == {"FINISHED"}
|
||||
restored = image_data.image_rgba(front["object"].data)
|
||||
np.testing.assert_allclose(restored[..., :3], expected, atol=1 / 255)
|
||||
np.testing.assert_array_equal(restored[..., 3], 1)
|
||||
assert np.all(np.diff(restored[0, :, 0]) <= 0)
|
||||
np.testing.assert_array_equal(restored[0, 0, :3], 1)
|
||||
|
||||
|
||||
def test_brush_release_commits_one_packed_image_without_touching_shared_source():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Mask Source", width=8, height=6, alpha=True)
|
||||
source_rgba = np.ones((6, 8, 4), dtype=np.float32)
|
||||
source_rgba[:, :, :3] = (0.2, 0.4, 0.6)
|
||||
source_image.pixels.foreach_set(np.flipud(source_rgba).ravel())
|
||||
source_image.update()
|
||||
source = bpy.data.objects.new("Mask Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = source_image
|
||||
source.empty_display_size = 3.0
|
||||
source.empty_image_offset = (-0.25, -0.75)
|
||||
source.matrix_world = Matrix.Translation((1.0, 2.0, 3.0))
|
||||
shared = bpy.data.objects.new("Mask Shared", None)
|
||||
shared.empty_display_type = "IMAGE"
|
||||
shared.data = source_image
|
||||
bpy.context.collection.objects.link(source)
|
||||
bpy.context.collection.objects.link(shared)
|
||||
matrix = source.matrix_world.copy()
|
||||
|
||||
selection_mask = SelectionMask(
|
||||
np.ones((2, 3), dtype=np.float32),
|
||||
(2, 1, 5, 3),
|
||||
)
|
||||
operator = SimpleNamespace(mode="SUBTRACT", report=lambda *_args: None)
|
||||
status = edit_mask.EditImageAlpha._apply_mask(
|
||||
operator,
|
||||
SimpleNamespace(),
|
||||
source,
|
||||
selection_mask,
|
||||
)
|
||||
|
||||
result = source.data
|
||||
assert status == {"FINISHED"}
|
||||
assert source.data == result
|
||||
assert result != source_image
|
||||
assert result.packed_file is not None
|
||||
assert shared.data == source_image
|
||||
np.testing.assert_allclose(image_data.image_rgba(source_image), source_rgba)
|
||||
result_rgba = image_data.image_rgba(result)
|
||||
np.testing.assert_array_equal(result_rgba[1:3, 2:5, 3], 0.0)
|
||||
assert source.matrix_world == matrix
|
||||
assert source.empty_display_size == 3.0
|
||||
assert tuple(source.empty_image_offset) == (-0.25, -0.75)
|
||||
|
||||
|
||||
def test_brush_drag_does_not_create_or_update_an_image():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source_image = bpy.data.images.new("Mask Cancel", width=4, height=3, alpha=True)
|
||||
source = bpy.data.objects.new("Mask Cancel", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = source_image
|
||||
bpy.context.collection.objects.link(source)
|
||||
image_count = len(bpy.data.images)
|
||||
shared = bpy.data.objects.new("Mask Cancel shared", None)
|
||||
shared.empty_display_type = "IMAGE"
|
||||
shared.data = source_image
|
||||
before = np.array(source_image.pixels[:])
|
||||
operator = edit_mask.ImageGesture()
|
||||
operator.active_tool_id = "anyimage.mask"
|
||||
operator.gesture = "BRUSH"
|
||||
operator._path = [(10.0, 10.0)]
|
||||
operator._path_samples = list(operator._path)
|
||||
operator._cursor = (10.0, 10.0)
|
||||
context = SimpleNamespace(area=SimpleNamespace(tag_redraw=lambda: None),
|
||||
workspace=SimpleNamespace(status_text_set=lambda value: None))
|
||||
event = SimpleNamespace(
|
||||
type="MOUSEMOVE",
|
||||
value="NOTHING",
|
||||
mouse_region_x=20,
|
||||
mouse_region_y=25,
|
||||
)
|
||||
|
||||
result = edit_mask.ImageGesture.modal(operator, context, event)
|
||||
|
||||
assert result == {"RUNNING_MODAL"}
|
||||
assert source.data == source_image
|
||||
assert len(bpy.data.images) == image_count
|
||||
event.type, event.value = "ESC", "PRESS"
|
||||
assert operator.modal(context, event) == {"CANCELLED"}
|
||||
assert source.data == shared.data == source_image
|
||||
np.testing.assert_array_equal(source_image.pixels[:], before)
|
||||
assert len(bpy.data.images) == image_count
|
||||
assert operator._path is operator._path_samples is operator._brush_preview is None
|
||||
|
||||
|
||||
def test_object_normal_image_follows_cutout_inward_axis():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new(
|
||||
@@ -222,7 +383,15 @@ def test_object_normal_image_follows_cutout_inward_axis():
|
||||
|
||||
actual = np.empty(4, dtype=np.float32)
|
||||
image.pixels.foreach_get(actual)
|
||||
expected = np.asarray((0.4, 0.8, -0.2), dtype=np.float32) * 0.5 + 0.5
|
||||
assert np.allclose(actual[:3], encoded, atol=1e-6)
|
||||
assert np.isclose(actual[3], 0.3)
|
||||
|
||||
cutout_object.orient_object_normal_image(
|
||||
image,
|
||||
cutout_object.CutoutInwardAxis.POSITIVE_X,
|
||||
)
|
||||
image.pixels.foreach_get(actual)
|
||||
expected = np.asarray((-0.8, -0.2, -0.4), dtype=np.float32) * 0.5 + 0.5
|
||||
assert np.allclose(actual[:3], expected, atol=1e-6)
|
||||
assert np.isclose(actual[3], 0.3)
|
||||
|
||||
@@ -255,6 +424,13 @@ def test_cutout_axes_produce_the_same_world_geometry(shape):
|
||||
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
|
||||
)
|
||||
|
||||
for result in (positive_x, negative_z):
|
||||
local = np.asarray([tuple(vertex.co) for vertex in result.data.vertices])
|
||||
assert np.allclose(local[:, 2], 0.0, atol=1e-8)
|
||||
assert np.ptp(local[:, 0]) > 0.0
|
||||
assert np.ptp(local[:, 1]) > 0.0
|
||||
assert all(polygon.normal.z > 0.99 for polygon in result.data.polygons)
|
||||
|
||||
def world_positions(obj):
|
||||
local = _evaluated_positions(obj)
|
||||
return np.asarray([tuple(obj.matrix_world @ Vector(point)) for point in local])
|
||||
@@ -393,6 +569,29 @@ def test_cutout_modifier_wraps_four_menu_controlled_shape_groups():
|
||||
and link.to_socket.name == "Menu"
|
||||
for link in cutout.links
|
||||
)
|
||||
axis_switch = next(
|
||||
node
|
||||
for node in cutout.nodes
|
||||
if node.bl_idname == "GeometryNodeMenuSwitch"
|
||||
and [item.name for item in node.enum_items] == ["-Z", "+X"]
|
||||
)
|
||||
negative_z_link = next(
|
||||
link
|
||||
for link in cutout.links
|
||||
if link.to_node == axis_switch and link.to_socket.name == "-Z"
|
||||
)
|
||||
positive_x_link = next(
|
||||
link
|
||||
for link in cutout.links
|
||||
if link.to_node == axis_switch and link.to_socket.name == "+X"
|
||||
)
|
||||
assert positive_x_link.from_node.bl_idname == "GeometryNodeTransform"
|
||||
assert any(
|
||||
link.from_socket == negative_z_link.from_socket
|
||||
and link.to_node == positive_x_link.from_node
|
||||
and link.to_socket.name == "Geometry"
|
||||
for link in cutout.links
|
||||
)
|
||||
|
||||
child_groups = {
|
||||
shape: bpy.data.node_groups[name]
|
||||
@@ -817,7 +1016,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
|
||||
8,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
|
||||
assert len(result.data.vertices) > 4
|
||||
assert len(result.data.polygons) > 1
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
@@ -845,7 +1044,7 @@ def test_balloon_build_starts_from_the_unified_single_sheet():
|
||||
assert normal_scale_link.from_node == normal_scale_attribute
|
||||
assert normal_scale_link.from_socket == normal_scale_attribute.outputs["Fac"]
|
||||
assert result.modifiers[0].node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
|
||||
inward = result.matrix_world.to_3x3() @ Vector((1.0, 0.0, 0.0))
|
||||
inward = result.matrix_world.to_3x3() @ Vector((0.0, 0.0, -1.0))
|
||||
assert np.allclose(tuple(inward), (0.0, 0.0, -1.0), atol=1e-6)
|
||||
|
||||
|
||||
@@ -991,7 +1190,7 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
depth_metadata=depth_metadata,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAME
|
||||
@@ -1030,9 +1229,9 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
baseline_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
baseline_x = np.asarray([vertex.co.x for vertex in baseline_mesh.vertices])
|
||||
baseline_min = baseline_x.min()
|
||||
baseline_max = baseline_x.max()
|
||||
baseline_z = np.asarray([vertex.co.z for vertex in baseline_mesh.vertices])
|
||||
baseline_min = baseline_z.min()
|
||||
baseline_max = baseline_z.max()
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
@@ -1047,9 +1246,9 @@ def test_depth_surface_protected_smooth_evaluates():
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
assert len(evaluated_mesh.polygons) > 0
|
||||
x = np.asarray([vertex.co.x for vertex in evaluated_mesh.vertices])
|
||||
assert np.isclose(x.min(), baseline_min, atol=1e-5)
|
||||
assert np.isclose(x.max(), baseline_max + 1.0, atol=1e-5)
|
||||
z = np.asarray([vertex.co.z for vertex in evaluated_mesh.vertices])
|
||||
assert np.isclose(z.min(), baseline_min - 1.0, atol=1e-5)
|
||||
assert np.isclose(z.max(), baseline_max, atol=1e-5)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
@@ -1112,9 +1311,9 @@ def test_depth_surface_keeps_continuous_points_across_zero_validity_edge():
|
||||
positions = _evaluated_positions(result)
|
||||
|
||||
assert np.isfinite(positions).all()
|
||||
assert np.ptp(positions[:, 0]) < 1e-4
|
||||
assert np.ptp(positions[:, 0]) > 1.5
|
||||
assert np.ptp(positions[:, 1]) > 1.5
|
||||
assert np.ptp(positions[:, 2]) > 1.5
|
||||
assert np.ptp(positions[:, 2]) < 1e-4
|
||||
|
||||
|
||||
def _evaluated_positions(obj):
|
||||
@@ -1324,7 +1523,7 @@ def _add_cutout_modifier(obj, shape):
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Inward Axis"),
|
||||
1,
|
||||
0,
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
@@ -1345,26 +1544,26 @@ def _add_cutout_modifier(obj, shape):
|
||||
|
||||
|
||||
def _depth_surface_stretch_object():
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
a = z * 3 + y
|
||||
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
|
||||
for y in range(2):
|
||||
for x in range(2):
|
||||
a = y * 3 + x
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
data = bpy.data.meshes.new("Depth Surface Stretch")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
uv_layer = data.uv_layers.new(name="UVMap")
|
||||
for loop in data.loops:
|
||||
coordinate = data.vertices[loop.vertex_index].co
|
||||
uv_layer.data[loop.index].uv = (coordinate.y * 0.5, coordinate.z * 0.5)
|
||||
uv_layer.data[loop.index].uv = (coordinate.x * 0.5, coordinate.y * 0.5)
|
||||
obj = bpy.data.objects.new("Depth Surface Stretch", data)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "DEPTH_SURFACE")
|
||||
|
||||
points = np.empty((3, 3, 3), dtype=np.float32)
|
||||
for z in range(3):
|
||||
for y in range(3):
|
||||
points[z, y] = (-float(y), -float(z), 1.0)
|
||||
for y in range(3):
|
||||
for x in range(3):
|
||||
points[y, x] = (float(x), -float(y), 1.0)
|
||||
points[0, 0, 2] = 6.0
|
||||
depth_image = _depth_image(points, np.ones((3, 3), dtype=bool))
|
||||
for name, value in (
|
||||
@@ -1452,12 +1651,12 @@ def test_depth_surface_stretch_limit_tracks_projection_and_cleans_loose_topology
|
||||
|
||||
def test_cutout_thickness_keeps_one_connected_shell():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
a = z * 3 + y
|
||||
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
|
||||
for y in range(2):
|
||||
for x in range(2):
|
||||
a = y * 3 + x
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
data = bpy.data.meshes.new("Connected Cutout")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
obj = bpy.data.objects.new("Connected Cutout", data)
|
||||
@@ -1480,20 +1679,20 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
try:
|
||||
assert len(result.vertices) == 18
|
||||
assert len(result.polygons) == 24
|
||||
x = np.asarray([vertex.co.x for vertex in result.vertices])
|
||||
assert np.isclose(x.min(), -0.5, atol=1e-6)
|
||||
assert np.isclose(x.max(), 0.5, atol=1e-6)
|
||||
z = np.asarray([vertex.co.z for vertex in result.vertices])
|
||||
assert np.isclose(z.min(), -0.5, atol=1e-6)
|
||||
assert np.isclose(z.max(), 0.5, atol=1e-6)
|
||||
front_faces = [
|
||||
polygon for polygon in result.polygons if polygon.normal.x < -0.9
|
||||
polygon for polygon in result.polygons if polygon.normal.z > 0.9
|
||||
]
|
||||
back_faces = [polygon for polygon in result.polygons if polygon.normal.x > 0.9]
|
||||
back_faces = [polygon for polygon in result.polygons if polygon.normal.z < -0.9]
|
||||
side_faces = [
|
||||
polygon for polygon in result.polygons if abs(polygon.normal.x) < 0.1
|
||||
polygon for polygon in result.polygons if abs(polygon.normal.z) < 0.1
|
||||
]
|
||||
assert len(front_faces) == 8
|
||||
assert len(back_faces) == 8
|
||||
assert len(side_faces) == 8
|
||||
center = Vector((0.0, 1.0, 1.0))
|
||||
center = Vector((1.0, 1.0, 0.0))
|
||||
for polygon in side_faces:
|
||||
radial = polygon.center - center
|
||||
assert polygon.normal.dot(radial) > 0.0
|
||||
@@ -1513,7 +1712,7 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
assert len(result.vertices) == 34
|
||||
assert len(result.polygons) == 40
|
||||
side_faces = [
|
||||
polygon for polygon in result.polygons if abs(polygon.normal.x) < 0.1
|
||||
polygon for polygon in result.polygons if abs(polygon.normal.z) < 0.1
|
||||
]
|
||||
assert len(side_faces) == 24
|
||||
for polygon in side_faces:
|
||||
@@ -1529,11 +1728,11 @@ def test_cutout_thickness_keeps_one_connected_shell():
|
||||
|
||||
def test_balloon_uses_injected_profile_at_default_height():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
a = z * 3 + y
|
||||
for y in range(2):
|
||||
for x in range(2):
|
||||
a = y * 3 + x
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
data = bpy.data.meshes.new("Balloon Profile")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
@@ -1559,11 +1758,11 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(obj)
|
||||
assert np.isclose(positions[:, 0].min(), -1.0, atol=1e-6)
|
||||
assert np.isclose(positions[:, 0].max(), 1.0, atol=1e-6)
|
||||
assert np.isclose(positions[:, 2].min(), -1.0, atol=1e-6)
|
||||
assert np.isclose(positions[:, 2].max(), 1.0, atol=1e-6)
|
||||
assert np.isclose(
|
||||
positions[:, 0].min(),
|
||||
-positions[:, 0].max(),
|
||||
positions[:, 2].min(),
|
||||
-positions[:, 2].max(),
|
||||
atol=1e-5,
|
||||
)
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
@@ -1583,9 +1782,9 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
assert len(result.polygons) == baseline_face_count
|
||||
x = np.asarray([vertex.co.x for vertex in result.vertices])
|
||||
assert np.isclose(x.min(), -2.0, atol=1e-6)
|
||||
assert np.isclose(x.max(), 2.0, atol=1e-6)
|
||||
z = np.asarray([vertex.co.z for vertex in result.vertices])
|
||||
assert np.isclose(z.min(), -2.0, atol=1e-6)
|
||||
assert np.isclose(z.max(), 2.0, atol=1e-6)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
object_data.set_modifier_input(
|
||||
@@ -1597,21 +1796,21 @@ def test_balloon_uses_injected_profile_at_default_height():
|
||||
bpy.context.view_layer.update()
|
||||
zero_positions = _evaluated_positions(obj)
|
||||
assert len(zero_positions) == len(vertices)
|
||||
assert np.allclose(zero_positions[:, 0], 0.0, atol=1e-6)
|
||||
assert np.allclose(zero_positions[:, 2], 0.0, atol=1e-6)
|
||||
|
||||
|
||||
def test_cutout_cleanup_uses_absolute_area_with_many_small_islands():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
for z in range(2):
|
||||
for y in range(2):
|
||||
a = z * 3 + y
|
||||
for y in range(2):
|
||||
for x in range(2):
|
||||
a = y * 3 + x
|
||||
faces.extend(((a, a + 1, a + 4), (a, a + 4, a + 3)))
|
||||
for island in range(12):
|
||||
start = len(vertices)
|
||||
y = 5.0 + island * 0.1
|
||||
vertices.extend(((0.0, y, 5.0), (0.0, y + 0.01, 5.0), (0.0, y, 5.01)))
|
||||
x = 5.0 + island * 0.1
|
||||
vertices.extend(((x, 5.0, 0.0), (x + 0.01, 5.0, 0.0), (x, 5.01, 0.0)))
|
||||
faces.append((start, start + 1, start + 2))
|
||||
data = bpy.data.meshes.new("Cleanup")
|
||||
data.from_pydata(vertices, (), faces)
|
||||
@@ -1674,7 +1873,7 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
selection_values=selection_values,
|
||||
)
|
||||
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
assert "o_balloon_depth" not in result.data.attributes
|
||||
assert "o_depth_offset" not in result.data.attributes
|
||||
@@ -1733,16 +1932,16 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(result)
|
||||
center_distance = np.linalg.norm(positions[:, 1:] - expected_center, axis=1)
|
||||
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, 0].min()
|
||||
depth_at_thickness_one = positions[nearest_center, 2].max()
|
||||
object_data.set_modifier_input(modifier, thickness_id, 2.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
positions = _evaluated_positions(result)
|
||||
center_distance = np.linalg.norm(positions[:, 1:] - expected_center, axis=1)
|
||||
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, 0].min()
|
||||
depth_at_thickness_two = positions[nearest_center, 2].max()
|
||||
assert np.isclose(
|
||||
depth_at_thickness_one,
|
||||
depth_at_thickness_two,
|
||||
@@ -1762,8 +1961,8 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
bpy.context.view_layer.update()
|
||||
after_positions = _evaluated_positions(result)
|
||||
assert np.isclose(
|
||||
(after_positions[:, 0] - before_positions[:, 0]).min(),
|
||||
-0.2,
|
||||
(after_positions[:, 2] - before_positions[:, 2]).max(),
|
||||
0.2,
|
||||
atol=1e-5,
|
||||
)
|
||||
|
||||
@@ -1797,14 +1996,14 @@ def test_depth_balloon_uses_metric_plane_inputs_without_controller():
|
||||
mirrored = _evaluated_positions(result)
|
||||
layers = {}
|
||||
for x, y, z in mirrored:
|
||||
layers.setdefault((round(float(y), 6), round(float(z), 6)), []).append(x)
|
||||
layers.setdefault((round(float(x), 6), round(float(y), 6)), []).append(z)
|
||||
assert layers
|
||||
for x_values in layers.values():
|
||||
if len(x_values) == 1:
|
||||
assert np.isclose(x_values[0], 0.0, atol=1e-5)
|
||||
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(x_values) + max(x_values),
|
||||
min(z_values) + max(z_values),
|
||||
0.0,
|
||||
atol=1e-5,
|
||||
)
|
||||
@@ -1990,7 +2189,7 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
assert len(result.data.vertices) > 4
|
||||
assert len(result.data.polygons) > 1
|
||||
assert all(polygon.use_smooth for polygon in result.data.polygons)
|
||||
assert all(abs(vertex.co.x) < 1e-8 for vertex in result.data.vertices)
|
||||
assert all(abs(vertex.co.z) < 1e-8 for vertex in result.data.vertices)
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
source_inverse = source.matrix_world.inverted()
|
||||
source_x = [
|
||||
|
||||
Reference in New Issue
Block a user