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:
562
tests/test_mesh_blender.py
Normal file
562
tests/test_mesh_blender.py
Normal file
@@ -0,0 +1,562 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Matrix
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.operators import material, mesh
|
||||
from anyimage.operators.normal_map import bake_tangent_normal_image
|
||||
|
||||
|
||||
def _inputs(group):
|
||||
return [
|
||||
item.name
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
||||
]
|
||||
|
||||
|
||||
def _group_node(group, node_tree):
|
||||
return next(
|
||||
node
|
||||
for node in node_tree.nodes
|
||||
if node.bl_idname == "ShaderNodeGroup" and node.node_tree == group
|
||||
)
|
||||
|
||||
|
||||
def test_tangent_bake_changes_pixels_using_evaluated_mesh_tbn():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
bpy.context.collection.objects.link(source)
|
||||
target_mesh = bpy.data.meshes.new("Target")
|
||||
target_mesh.from_pydata(
|
||||
((0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)),
|
||||
(),
|
||||
((0, 1, 2),),
|
||||
)
|
||||
target_mesh.polygons[0].use_smooth = True
|
||||
mesh._assign_uv(
|
||||
target_mesh,
|
||||
np.asarray(((0.0, 0.0), (1.0, 0.0), (0.0, 1.0))),
|
||||
)
|
||||
target = bpy.data.objects.new("Target", target_mesh)
|
||||
bpy.context.collection.objects.link(target)
|
||||
target.matrix_world = Matrix.Rotation(np.radians(-90.0), 4, "Y")
|
||||
image = bpy.data.images.new("Object Normal", width=3, height=3, alpha=True)
|
||||
pixels = np.ones((3, 3, 4), dtype=np.float32)
|
||||
pixels[..., :3] = (1.0, 0.5, 0.5)
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
|
||||
bake_tangent_normal_image(bpy.context, source, target, image)
|
||||
|
||||
baked = np.empty(3 * 3 * 4, dtype=np.float32)
|
||||
image.pixels.foreach_get(baked)
|
||||
baked = baked.reshape((3, 3, 4))
|
||||
assert not np.allclose(baked[0, 0, :3], pixels[0, 0, :3])
|
||||
|
||||
|
||||
def test_shape_node_groups_expose_only_shape_parameters():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
plane = mesh._mesh_node_group(mesh.PLANE)
|
||||
depth_plane = mesh._mesh_node_group(mesh.DEPTH_PLANE)
|
||||
cutout = mesh._mesh_node_group(mesh.CUTOUT)
|
||||
depth_surface = mesh._mesh_node_group(mesh.DEPTH_SURFACE)
|
||||
|
||||
assert plane.name == "O Mesh Plane"
|
||||
assert _inputs(plane) == ["Geometry", "Subdivide", "Thickness", "Material"]
|
||||
assert _inputs(cutout) == ["Geometry", "Thickness"]
|
||||
assert _inputs(depth_plane) == [
|
||||
"Geometry",
|
||||
"Subdivide",
|
||||
"Thickness",
|
||||
"Depth Amount",
|
||||
"Depth Image",
|
||||
"Material",
|
||||
]
|
||||
assert _inputs(depth_surface) == [
|
||||
"Geometry",
|
||||
"Thickness",
|
||||
"Depth Scale",
|
||||
"Iterations",
|
||||
"Smooth",
|
||||
"Depth Continuity",
|
||||
]
|
||||
for group in (depth_plane, cutout, depth_surface):
|
||||
thickness = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Thickness"
|
||||
)
|
||||
assert thickness.subtype == "DISTANCE"
|
||||
for group, name in (
|
||||
(depth_plane, "Depth Amount"),
|
||||
(depth_surface, "Depth Scale"),
|
||||
):
|
||||
depth_amount = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == name
|
||||
)
|
||||
assert depth_amount.subtype == "NONE"
|
||||
assert depth_amount.min_value == 0.0
|
||||
assert depth_amount.max_value > 1e20
|
||||
assert any(
|
||||
node.bl_idname == "ShaderNodeMath" and node.operation == "MAXIMUM"
|
||||
for node in depth_plane.nodes
|
||||
)
|
||||
assert any(node.bl_idname == "GeometryNodeMeshGrid" for node in plane.nodes)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeGroup" and node.node_tree == plane
|
||||
for node in depth_plane.nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeDeleteGeometry" for node in depth_plane.nodes
|
||||
)
|
||||
smooth = next(
|
||||
item
|
||||
for item in depth_surface.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Smooth"
|
||||
)
|
||||
assert smooth.default_value == 1.0
|
||||
iterations = next(
|
||||
item
|
||||
for item in depth_surface.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Iterations"
|
||||
)
|
||||
assert iterations.default_value == 1
|
||||
assert iterations.min_value == 0
|
||||
continuity = next(
|
||||
item
|
||||
for item in depth_surface.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.name == "Depth Continuity"
|
||||
)
|
||||
assert continuity.default_value == 0.0
|
||||
assert not any(node.bl_idname == "GeometryNodeGroup" for node in depth_surface.nodes)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeRepeatInput" for node in depth_surface.nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeRepeatOutput" for node in depth_surface.nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeBlurAttribute" for node in depth_surface.nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeInputMeshEdgeNeighbors"
|
||||
for node in depth_surface.nodes
|
||||
)
|
||||
assert sum(
|
||||
node.bl_idname == "GeometryNodeInputMeshEdgeNeighbors"
|
||||
for node in depth_surface.nodes
|
||||
) == 2
|
||||
assert sum(
|
||||
node.bl_idname == "GeometryNodeInputMeshVertexNeighbors"
|
||||
for node in depth_surface.nodes
|
||||
) == 2
|
||||
boundary_compares = [
|
||||
node
|
||||
for node in depth_surface.nodes
|
||||
if node.bl_idname == "FunctionNodeCompare"
|
||||
and node.data_type == "INT"
|
||||
and node.operation == "EQUAL"
|
||||
]
|
||||
assert len(boundary_compares) == 4
|
||||
for compare in boundary_compares:
|
||||
compare_a = next(
|
||||
socket for socket in compare.inputs if socket.identifier == "A_INT"
|
||||
)
|
||||
compare_b = next(
|
||||
socket for socket in compare.inputs if socket.identifier == "B_INT"
|
||||
)
|
||||
assert compare_a.is_linked
|
||||
assert compare_b.default_value == 1
|
||||
fixed_pin_switches = [
|
||||
node
|
||||
for node in depth_surface.nodes
|
||||
if node.bl_idname == "GeometryNodeSwitch"
|
||||
and not node.inputs["Switch"].is_linked
|
||||
]
|
||||
assert len(fixed_pin_switches) == 2
|
||||
assert all(node.inputs["Switch"].default_value for node in fixed_pin_switches)
|
||||
assert any(
|
||||
node.bl_idname == "ShaderNodeMix"
|
||||
and not node.inputs[0].is_linked
|
||||
and node.inputs[0].default_value == 1.0
|
||||
for node in depth_surface.nodes
|
||||
)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeInputNamedAttribute"
|
||||
and node.inputs["Name"].default_value == mesh.DEPTH_DISCONTINUITY_ATTRIBUTE
|
||||
for node in depth_surface.nodes
|
||||
)
|
||||
for group in (depth_plane, cutout, depth_surface):
|
||||
input_sockets = [
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"
|
||||
]
|
||||
assert all(item.structure_type == "SINGLE" for item in input_sockets)
|
||||
for group_input in (
|
||||
node for node in group.nodes if node.bl_idname == "NodeGroupInput"
|
||||
):
|
||||
assert all(
|
||||
output.is_linked or output.hide
|
||||
for output in group_input.outputs
|
||||
if output.name
|
||||
)
|
||||
geometry_links = [
|
||||
link
|
||||
for link in group.links
|
||||
if link.from_socket.type == "GEOMETRY"
|
||||
and link.to_socket.type == "GEOMETRY"
|
||||
]
|
||||
assert all(
|
||||
link.from_node.location.x <= link.to_node.location.x
|
||||
for link in geometry_links
|
||||
)
|
||||
assert all(not node.label for node in group.nodes)
|
||||
node_locations = {tuple(node.location) for node in depth_surface.nodes}
|
||||
assert len(node_locations) == len(depth_surface.nodes)
|
||||
depth_plane_locations = {tuple(node.location) for node in depth_plane.nodes}
|
||||
assert len(depth_plane_locations) == len(depth_plane.nodes)
|
||||
assert any(
|
||||
node.bl_idname == "GeometryNodeSetShadeSmooth"
|
||||
and node.inputs["Shade Smooth"].default_value
|
||||
for node in depth_surface.nodes
|
||||
)
|
||||
|
||||
|
||||
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
assert material.SURFACE_MATERIAL_NODE_GROUP_NAME == "O Image Layer"
|
||||
assert (
|
||||
material.MOGE_SURFACE_MATERIAL_NODE_GROUP_NAME
|
||||
== material.BALLOON_MATERIAL_NODE_GROUP_NAME
|
||||
== material.IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
)
|
||||
group = material.material_node_group(
|
||||
surface_group_name=material.IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
)
|
||||
normal = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == "Normal"
|
||||
)
|
||||
assert tuple(normal.default_value) == (0.5, 0.5, 1.0, 1.0)
|
||||
bump_scale = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == "Bump Scale"
|
||||
)
|
||||
assert bump_scale.default_value == 0.0
|
||||
normal_scale = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == "Normal Scale"
|
||||
)
|
||||
assert normal_scale.default_value == 1.0
|
||||
object_space = next(
|
||||
item
|
||||
for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET"
|
||||
and item.in_out == "INPUT"
|
||||
and item.name == "Object Space"
|
||||
)
|
||||
assert object_space.default_value is False
|
||||
normal_maps = {
|
||||
node.space
|
||||
for node in group.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
}
|
||||
assert normal_maps == {"TANGENT", "OBJECT"}
|
||||
assert all(
|
||||
not node.label
|
||||
for node in group.nodes
|
||||
if node.bl_idname in {"ShaderNodeNormalMap", "ShaderNodeMix"}
|
||||
)
|
||||
|
||||
color = bpy.data.images.new("Color", width=2, height=2)
|
||||
normal_image = bpy.data.images.new("Normal", width=2, height=2)
|
||||
object_material = material.create_material(
|
||||
"Object Normal",
|
||||
color,
|
||||
1.0,
|
||||
surface_group_name=material.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
normal_image=normal_image,
|
||||
normal_space="OBJECT",
|
||||
)
|
||||
layer = _group_node(group, object_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is True
|
||||
|
||||
tangent_material = material.create_material(
|
||||
"Tangent Normal",
|
||||
color,
|
||||
1.0,
|
||||
surface_group_name=material.IMAGE_MATERIAL_NODE_GROUP_NAME,
|
||||
normal_image=normal_image,
|
||||
)
|
||||
layer = _group_node(group, tangent_material.node_tree)
|
||||
assert layer.inputs["Object Space"].default_value is False
|
||||
reused = material.material_node_group(
|
||||
surface_group_name=material.IMAGE_MATERIAL_NODE_GROUP_NAME
|
||||
)
|
||||
assert {
|
||||
node.space
|
||||
for node in reused.nodes
|
||||
if node.bl_idname == "ShaderNodeNormalMap"
|
||||
} == {"TANGENT", "OBJECT"}
|
||||
|
||||
|
||||
def test_build_plane_keeps_one_quad_and_generates_topology_in_geometry_nodes():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Source", width=4096, height=2048)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
material = bpy.data.materials.new("Source.material")
|
||||
|
||||
result = mesh._build_mesh_object(
|
||||
bpy.context,
|
||||
source,
|
||||
mesh.PLANE,
|
||||
2,
|
||||
material,
|
||||
keep_original=True,
|
||||
)
|
||||
|
||||
assert result["anyimage_mesh_shape"] == mesh.PLANE
|
||||
assert len(result.data.vertices) == 4
|
||||
assert len(result.data.polygons) == 1
|
||||
assert all(polygon.use_smooth for polygon in result.data.polygons)
|
||||
assert mesh.PROFILE_ATTRIBUTE not in result.data.attributes
|
||||
assert mesh.DEPTH_ATTRIBUTE not in result.data.attributes
|
||||
assert mesh.FRONT_FACE_ATTRIBUTE not in result.data.attributes
|
||||
assert result.modifiers[0].node_group.name == "O Mesh Plane"
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
assert len(evaluated_mesh.polygons) > 0
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
def test_depth_surface_protected_smooth_evaluates():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Surface", width=64, height=64)
|
||||
source = bpy.data.objects.new("Surface", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 2.0
|
||||
bpy.context.collection.objects.link(source)
|
||||
surface_material = bpy.data.materials.new("Surface.material")
|
||||
|
||||
size = 8
|
||||
depth = np.ones((size, size), dtype=np.float32)
|
||||
x, y = np.meshgrid(np.arange(size), np.arange(size))
|
||||
intrinsics = np.asarray(
|
||||
((8.0, 0.0, 3.5), (0.0, 8.0, 3.5), (0.0, 0.0, 1.0)),
|
||||
dtype=np.float32,
|
||||
)
|
||||
points = np.empty((size, size, 3), dtype=np.float32)
|
||||
points[..., 0] = (x - intrinsics[0, 2]) / intrinsics[0, 0] * depth
|
||||
points[..., 1] = (y - intrinsics[1, 2]) / intrinsics[1, 1] * depth
|
||||
points[..., 2] = depth
|
||||
field = (
|
||||
points,
|
||||
intrinsics,
|
||||
np.ones((size, size), dtype=bool),
|
||||
(size, size),
|
||||
1.0,
|
||||
)
|
||||
|
||||
result = mesh._build_mesh_object(
|
||||
bpy.context,
|
||||
source,
|
||||
mesh.DEPTH_SURFACE,
|
||||
8,
|
||||
surface_material,
|
||||
field=field,
|
||||
regions=((((0, 0), (64, 0), (64, 64), (0, 64)),),),
|
||||
image_size=(64, 64),
|
||||
keep_original=True,
|
||||
)
|
||||
|
||||
assert mesh.BOUNDARY_ATTRIBUTE in result.data.attributes
|
||||
assert mesh.DEPTH_PROTECTION_ATTRIBUTE in result.data.attributes
|
||||
modifier = result.modifiers[0]
|
||||
mesh.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
0.0,
|
||||
)
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
baseline_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
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()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Thickness"),
|
||||
1.0,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
evaluated_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
assert len(evaluated_mesh.polygons) > 0
|
||||
z = np.asarray([vertex.co.z for vertex in evaluated_mesh.vertices])
|
||||
assert np.isclose(z.max(), baseline_max, atol=1e-5)
|
||||
assert np.isclose(z.min(), baseline_min - 1.0, atol=1e-5)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Iterations"),
|
||||
4,
|
||||
)
|
||||
mesh.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Smooth"),
|
||||
1.0,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
internal_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
internal_positions = np.asarray(
|
||||
[tuple(vertex.co) for vertex in internal_mesh.vertices]
|
||||
)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
mesh.set_modifier_input(
|
||||
modifier,
|
||||
mesh.modifier_input_identifier(modifier.node_group, "Iterations"),
|
||||
0,
|
||||
)
|
||||
external_group = bpy.data.node_groups.new("External O Smooth", "GeometryNodeTree")
|
||||
external_group.is_modifier = True
|
||||
mesh._interface_socket(
|
||||
external_group, "Geometry", "OUTPUT", "NodeSocketGeometry"
|
||||
)
|
||||
mesh._interface_socket(
|
||||
external_group, "Geometry", "INPUT", "NodeSocketGeometry"
|
||||
)
|
||||
external_iterations = mesh._interface_socket(
|
||||
external_group, "Iterations", "INPUT", "NodeSocketInt"
|
||||
)
|
||||
external_iterations.default_value = 4
|
||||
external_iterations.min_value = 0
|
||||
external_smooth = mesh._interface_socket(
|
||||
external_group, "Smooth", "INPUT", "NodeSocketFloat"
|
||||
)
|
||||
external_smooth.default_value = 1.0
|
||||
external_input = external_group.nodes.new("NodeGroupInput")
|
||||
external_output = external_group.nodes.new("NodeGroupOutput")
|
||||
external_geometry = mesh._expand_smooth(
|
||||
external_group, external_input.outputs["Geometry"]
|
||||
)
|
||||
external_group.links.new(
|
||||
external_geometry, external_output.inputs["Geometry"]
|
||||
)
|
||||
external_modifier = result.modifiers.new("External O Smooth", "NODES")
|
||||
external_modifier.node_group = external_group
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
evaluated = result.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
external_mesh = evaluated.to_mesh()
|
||||
try:
|
||||
external_positions = np.asarray(
|
||||
[tuple(vertex.co) for vertex in external_mesh.vertices]
|
||||
)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
assert internal_positions.shape == external_positions.shape
|
||||
assert np.allclose(internal_positions, external_positions, atol=1e-6)
|
||||
|
||||
|
||||
def test_converted_color_images_are_new_and_keep_color_as_terminal_suffix():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Poster", width=16, height=16)
|
||||
source = bpy.data.objects.new("Poster", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
|
||||
first = mesh._copy_color_image(source)
|
||||
second = mesh._copy_color_image(source)
|
||||
|
||||
assert first is not image
|
||||
assert second is not image
|
||||
assert first is not second
|
||||
assert first.name.endswith(".color")
|
||||
assert second.name.endswith(".color")
|
||||
|
||||
|
||||
def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = bpy.data.images.new("Local Cutout", width=64, height=48, alpha=True)
|
||||
pixels = np.ones((48, 64, 4), dtype=np.float32)
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
source = bpy.data.objects.new("Local Cutout", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
source.select_set(True)
|
||||
bpy.context.view_layer.objects.active = source
|
||||
selection = "[[-8,8],[72,8],[72,40],[-8,40]]"
|
||||
source_bounds = mesh.plane_bounds(source)
|
||||
|
||||
with patch.object(mesh, "push_undo") as undo:
|
||||
mesh._build_local_cutout(
|
||||
SimpleNamespace(mesh_detail=16),
|
||||
bpy.context,
|
||||
source,
|
||||
mesh.CUTOUT,
|
||||
selection,
|
||||
)
|
||||
|
||||
result = bpy.context.view_layer.objects.active
|
||||
assert result is not source
|
||||
assert result.type == "MESH"
|
||||
assert result["anyimage_mesh_shape"] == mesh.CUTOUT
|
||||
assert len(result.data.vertices) > 4
|
||||
assert len(result.data.polygons) > 1
|
||||
assert all(polygon.use_smooth for polygon in result.data.polygons)
|
||||
vertex_x = [vertex.co.x for vertex in result.data.vertices]
|
||||
assert min(vertex_x) < source_bounds[0]
|
||||
assert max(vertex_x) > source_bounds[1]
|
||||
assert bpy.data.objects.get(source.name) is source
|
||||
assert result.data.materials[0].node_tree is not None
|
||||
color_texture = next(
|
||||
node
|
||||
for node in result.data.materials[0].node_tree.nodes
|
||||
if node.bl_idname == "ShaderNodeTexImage"
|
||||
)
|
||||
assert color_texture.image is not image
|
||||
copied = np.empty(48 * 64 * 4, dtype=np.float32)
|
||||
color_texture.image.pixels.foreach_get(copied)
|
||||
assert np.allclose(copied.reshape((48, 64, 4)), pixels)
|
||||
undo.assert_called_once_with("Convert to Cutout")
|
||||
Reference in New Issue
Block a user