Add tests for image layer, image plane, inventory, saved layout, commands, documentation, manifest, and packaging
- Implement tests for image layer functionality including alpha curve and depth layer behavior. - Create tests for image plane evaluation and modifier input handling. - Add tests to verify the saved asset inventory and its persistence. - Introduce tests for the documentation command to ensure correct project version deployment. - Validate the extension manifest structure and required permissions. - Develop tests for the packaging process, ensuring correct platform handling and dependency management. - Enhance image depth plane group functionality with new split threshold input and surface processing.
This commit is contained in:
0
tests/support/__init__.py
Normal file
0
tests/support/__init__.py
Normal file
186
tests/support/blender.py
Normal file
186
tests/support/blender.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib
|
||||
import builtins
|
||||
import sys
|
||||
import unittest
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
|
||||
|
||||
def mock_bpy_imports(fake_bpy):
|
||||
"""Stub Python imports while preserving Blender's native module identity."""
|
||||
original_import = builtins.__import__
|
||||
|
||||
def import_with_fake_bpy(name, globals=None, locals=None, fromlist=(), level=0):
|
||||
if name == "bpy" and level == 0:
|
||||
return fake_bpy
|
||||
return original_import(name, globals, locals, fromlist, level)
|
||||
|
||||
return patch("builtins.__import__", import_with_fake_bpy)
|
||||
|
||||
|
||||
class BlenderTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.previous_addon_modules = {
|
||||
name: module for name, module in sys.modules.items()
|
||||
if name == "anyimage" or name.startswith("anyimage.")
|
||||
}
|
||||
cls.remove_modules()
|
||||
|
||||
def draw_status_bar(_owner, _context):
|
||||
pass
|
||||
|
||||
draw_status_bar._draw_funcs = [draw_status_bar]
|
||||
|
||||
def append_status_bar(draw):
|
||||
draw._owner = "anyimage"
|
||||
draw_status_bar._draw_funcs.append(draw)
|
||||
cls.status_bar_draws.append(draw)
|
||||
|
||||
def remove_status_bar(draw):
|
||||
draw_status_bar._draw_funcs.remove(draw)
|
||||
cls.status_bar_draws.remove(draw)
|
||||
|
||||
fake_bpy = ModuleType("bpy")
|
||||
fake_bpy.props = SimpleNamespace(
|
||||
BoolProperty=lambda **options: options,
|
||||
EnumProperty=lambda **options: options,
|
||||
FloatProperty=lambda **options: options,
|
||||
FloatVectorProperty=lambda **options: options,
|
||||
IntProperty=lambda **options: options,
|
||||
PointerProperty=lambda **options: options,
|
||||
StringProperty=lambda **options: options,
|
||||
)
|
||||
fake_bpy.types = SimpleNamespace(
|
||||
AddonPreferences=object,
|
||||
Menu=object,
|
||||
Operator=object,
|
||||
Panel=object,
|
||||
PropertyGroup=object,
|
||||
WorkSpaceTool=object,
|
||||
Image=type("Image", (), {}),
|
||||
Scene=type("Scene", (), {}),
|
||||
STATUSBAR_HT_header=SimpleNamespace(
|
||||
draw=draw_status_bar,
|
||||
append=append_status_bar,
|
||||
remove=remove_status_bar,
|
||||
),
|
||||
VIEW3D_MT_object_context_menu=SimpleNamespace(
|
||||
append=lambda draw: cls.context_menu_draws.append(draw),
|
||||
prepend=lambda draw: cls.context_menu_draws.insert(0, draw),
|
||||
remove=lambda draw: cls.context_menu_draws.remove(draw),
|
||||
),
|
||||
OUTLINER_MT_object=SimpleNamespace(
|
||||
append=lambda draw: cls.outliner_menu_draws.append(draw),
|
||||
prepend=lambda draw: cls.outliner_menu_draws.insert(0, draw),
|
||||
remove=lambda draw: cls.outliner_menu_draws.remove(draw),
|
||||
),
|
||||
)
|
||||
cls.registered = []
|
||||
cls.registered_tools = []
|
||||
cls.status_bar_draws = []
|
||||
cls.context_menu_draws = []
|
||||
cls.outliner_menu_draws = []
|
||||
fake_bpy.utils = SimpleNamespace(
|
||||
register_class=cls.registered.append,
|
||||
unregister_class=cls.registered.remove,
|
||||
register_tool=lambda tool, **options: cls.registered_tools.append(
|
||||
(tool, options)
|
||||
),
|
||||
unregister_tool=lambda tool: cls.registered_tools.remove(
|
||||
next(item for item in cls.registered_tools if item[0] is tool)
|
||||
),
|
||||
)
|
||||
fake_bpy.context = SimpleNamespace(
|
||||
preferences=SimpleNamespace(addons={}),
|
||||
)
|
||||
fake_bpy.app = SimpleNamespace(online_access=True)
|
||||
fake_bpy.path = SimpleNamespace(abspath=lambda path: path)
|
||||
fake_bpy.data = SimpleNamespace(
|
||||
objects=SimpleNamespace(get=lambda _name: None),
|
||||
)
|
||||
cls.bpy_import_patch = mock_bpy_imports(fake_bpy)
|
||||
cls.bpy_import_patch.start()
|
||||
cls.addClassCleanup(cls.bpy_import_patch.stop)
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
cls.paths_before_import = tuple(sys.path)
|
||||
cls.anyimage = importlib.import_module("anyimage")
|
||||
cls.runtime = importlib.import_module("anyimage.runtime")
|
||||
cls.ai_setup = importlib.import_module("anyimage.operators.ai_setup")
|
||||
cls.ai = importlib.import_module("anyimage.common.ai")
|
||||
cls.image_data = importlib.import_module("anyimage.common.image")
|
||||
cls.convert_to_plane = importlib.import_module(
|
||||
"anyimage.operators.convert_to_plane.operators"
|
||||
)
|
||||
cls.convert_to_plane_object = importlib.import_module(
|
||||
"anyimage.operators.convert_to_plane.object"
|
||||
)
|
||||
cls.cutout_main = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.operators"
|
||||
)
|
||||
cls.remove_background = importlib.import_module(
|
||||
"anyimage.operators.remove_background"
|
||||
)
|
||||
cls.upscale = importlib.import_module("anyimage.operators.upscale")
|
||||
cls.image_interaction = importlib.import_module("anyimage.common.viewport")
|
||||
cls.edit_mask = importlib.import_module(
|
||||
"anyimage.operators.mask_tool"
|
||||
)
|
||||
cls.image_selection = importlib.import_module("anyimage.common.selection")
|
||||
cls.image_frame = importlib.import_module(
|
||||
"anyimage.operators.frame_tool"
|
||||
)
|
||||
cls.rectify = importlib.import_module(
|
||||
"anyimage.operators.rectify_tool"
|
||||
)
|
||||
cls.cutout_geometry = importlib.import_module(
|
||||
"anyimage.operators.cutout_tool.geometry"
|
||||
)
|
||||
cls.paths_after_import = tuple(sys.path)
|
||||
cls.fake_bpy = fake_bpy
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
sys.path.remove(str(SOURCE_ROOT))
|
||||
cls.remove_modules()
|
||||
|
||||
sys.modules.update(cls.previous_addon_modules)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def remove_modules():
|
||||
for module_name in tuple(sys.modules):
|
||||
if module_name == "anyimage" or module_name.startswith("anyimage."):
|
||||
sys.modules.pop(module_name)
|
||||
|
||||
|
||||
def setUp(self):
|
||||
preferences = SimpleNamespace(
|
||||
storage_root="",
|
||||
device="CUDA",
|
||||
upscale_model="REALESRGAN_GENERAL_WDN_X4V3",
|
||||
max_ai_input_size=2048,
|
||||
)
|
||||
self.fake_bpy.context.preferences.addons = {
|
||||
"anyimage": SimpleNamespace(preferences=preferences)
|
||||
}
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
self.anyimage.runtime.close_active()
|
||||
self.registered.clear()
|
||||
self.registered_tools.clear()
|
||||
self.status_bar_draws.clear()
|
||||
self.context_menu_draws.clear()
|
||||
self.outliner_menu_draws.clear()
|
||||
self.fake_bpy.data = SimpleNamespace(
|
||||
objects=SimpleNamespace(get=lambda _name: None),
|
||||
)
|
||||
if hasattr(self.fake_bpy.types.Scene, "anyimage_settings"):
|
||||
del self.fake_bpy.types.Scene.anyimage_settings
|
||||
if hasattr(self.fake_bpy.types.Image, "anyimage_upscale_source"):
|
||||
del self.fake_bpy.types.Image.anyimage_upscale_source
|
||||
113
tests/support/depth_surface.py
Normal file
113
tests/support/depth_surface.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||
|
||||
from collections import Counter
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
from tests.support.nodes import CUTOUT_BUILDERS
|
||||
|
||||
|
||||
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = CUTOUT_BUILDERS["DEPTH_SOLID"]()
|
||||
# Two columns with a complete depth jump along their common boundary.
|
||||
points = [
|
||||
(x * size / resolution, y * size / resolution, 0)
|
||||
for y in range(resolution + 1)
|
||||
for x in range(2 * resolution + 1)
|
||||
]
|
||||
stride = 2 * resolution + 1
|
||||
faces = [
|
||||
(
|
||||
y * stride + x,
|
||||
y * stride + x + 1,
|
||||
(y + 1) * stride + x + 1,
|
||||
(y + 1) * stride + x,
|
||||
)
|
||||
for y in range(resolution)
|
||||
for x in range(2 * resolution)
|
||||
]
|
||||
mesh = bpy.data.meshes.new("Step")
|
||||
mesh.from_pydata(points, [], faces)
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
for loop in mesh.loops:
|
||||
x, y, _ = points[loop.vertex_index]
|
||||
uv.data[loop.index].uv = (x / (2 * size), y / size)
|
||||
obj = bpy.data.objects.new("Step", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
image = bpy.data.images.new("Camera", width=64, height=32, float_buffer=True)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
yy, xx = np.mgrid[:32, :64]
|
||||
pixels = np.ones((32, 64, 4), np.float32)
|
||||
pixels[..., 0] = (xx + 0.5) / 32
|
||||
pixels[..., 1] = -(yy + 0.5) / 32
|
||||
pixels[..., 2] = 1 + (xx >= 32) * step
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
modifier = obj.modifiers.new("Depth Surface", "NODES")
|
||||
modifier.node_group = group
|
||||
inputs = {
|
||||
s.name: s.identifier
|
||||
for s in group.interface.items_tree
|
||||
if s.item_type == "SOCKET" and s.in_out == "INPUT"
|
||||
}
|
||||
|
||||
def set_value(name, value):
|
||||
modifier[inputs[name]] = value
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
for name, value in (
|
||||
("Depth Image", image),
|
||||
("Uniform Scale", size),
|
||||
("Reference Depth", size),
|
||||
("Depth Scale", 1.0),
|
||||
("Thickness", 0.0),
|
||||
("Front Inflation", 0.0),
|
||||
("Split Threshold", 0.0),
|
||||
("Mode", 1),
|
||||
("Smooth", 0),
|
||||
):
|
||||
set_value(name, value)
|
||||
if wrapper:
|
||||
set_value("Cleanup Threshold", 0.0)
|
||||
set_value("Inward Axis", 0)
|
||||
set_value("Smooth", 0)
|
||||
return obj, set_value
|
||||
|
||||
|
||||
def evaluated(obj):
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
return np.array([v.co[:] for v in mesh.vertices]), [
|
||||
tuple(p.vertices) for p in mesh.polygons
|
||||
]
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
def assert_closed(faces):
|
||||
edges = Counter(
|
||||
tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0]))
|
||||
)
|
||||
assert edges and set(edges.values()) == {2}, Counter(edges.values())
|
||||
|
||||
|
||||
def component_count(faces):
|
||||
adjacency = {}
|
||||
for face in faces:
|
||||
for a, b in zip(face, (*face[1:], face[0])):
|
||||
adjacency.setdefault(a, set()).add(b)
|
||||
adjacency.setdefault(b, set()).add(a)
|
||||
remaining = set(adjacency)
|
||||
count = 0
|
||||
while remaining:
|
||||
count += 1
|
||||
pending = [remaining.pop()]
|
||||
while pending:
|
||||
for neighbor in adjacency[pending.pop()] & remaining:
|
||||
remaining.remove(neighbor)
|
||||
pending.append(neighbor)
|
||||
return count
|
||||
207
tests/support/image_objects.py
Normal file
207
tests/support/image_objects.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import object as object_data
|
||||
from anyimage.common.selection import SelectionMask
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
|
||||
|
||||
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 _create_cutout_shape(
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
*,
|
||||
bounds=None,
|
||||
depth_image=None,
|
||||
depth_metadata=None,
|
||||
selection_values=None,
|
||||
inward_axis=cutout_object.CutoutInwardAxis.NEGATIVE_Z,
|
||||
):
|
||||
if bounds is None:
|
||||
bounds = (0, 0, *tuple(int(value) for value in source.data.size))
|
||||
left, top, right, bottom = bounds
|
||||
if selection_values is None:
|
||||
selection_values = np.ones((bottom - top, right - left), dtype=np.float32)
|
||||
selection_mask = SelectionMask(selection_values, bounds)
|
||||
source_rgba = image_data.image_rgba(source.data)
|
||||
color_image = cutout_object.create_cropped_color_image(
|
||||
source,
|
||||
selection_mask.bounds,
|
||||
source_rgba,
|
||||
)
|
||||
base_shape = cutout_geometry.build_base_shape(
|
||||
bpy.context,
|
||||
source,
|
||||
detail,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
)
|
||||
return cutout_object.create_shape_object(
|
||||
bpy.context,
|
||||
source,
|
||||
shape,
|
||||
detail,
|
||||
selection_mask.values,
|
||||
selection_mask.bounds,
|
||||
base_shape,
|
||||
depth_image=depth_image,
|
||||
depth_metadata=depth_metadata,
|
||||
inward_axis=inward_axis,
|
||||
color_image=color_image,
|
||||
)
|
||||
|
||||
|
||||
def _depth_image(points, valid, name="Depth"):
|
||||
points = np.asarray(points, dtype=np.float32)
|
||||
valid = np.asarray(valid, dtype=bool)
|
||||
height, width = valid.shape
|
||||
pixels = np.zeros((height, width, 4), dtype=np.float32)
|
||||
pixels[..., :3] = points
|
||||
pixels[..., 3] = valid
|
||||
image = bpy.data.images.new(
|
||||
name,
|
||||
width=width,
|
||||
height=height,
|
||||
alpha=True,
|
||||
float_buffer=True,
|
||||
)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
image.pixels.foreach_set(np.flipud(pixels).ravel())
|
||||
image.pack()
|
||||
return image
|
||||
|
||||
|
||||
def _depth_metadata(intrinsics, image_size, reference_depth):
|
||||
return {
|
||||
"intrinsics": tuple(tuple(float(value) for value in row) for row in intrinsics),
|
||||
"image_size": tuple(int(value) for value in image_size),
|
||||
"reference_depth": float(reference_depth),
|
||||
}
|
||||
|
||||
|
||||
def _evaluated_positions(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
return np.asarray([tuple(vertex.co) for vertex in result.vertices])
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
|
||||
|
||||
def _add_cutout_modifier(obj, shape):
|
||||
group = cutout_object.load_cutout_node_group(shape)
|
||||
modifier = obj.modifiers.new(group.name, "NODES")
|
||||
modifier.node_group = group
|
||||
if shape in {"FLAT", "DEPTH_SOLID"}:
|
||||
object_data.set_modifier_input(modifier, object_data.modifier_input_identifier(group, "Mode"), 1)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Inward Axis"),
|
||||
0,
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Cleanup Threshold"),
|
||||
0.0,
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Smooth Weight"),
|
||||
0.0,
|
||||
)
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(group, "Smooth"),
|
||||
1,
|
||||
)
|
||||
return modifier
|
||||
|
||||
|
||||
def _depth_surface_step_object():
|
||||
vertices = [(float(x), float(y), 0.0) for y in range(3) for x in range(3)]
|
||||
faces = []
|
||||
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 Step")
|
||||
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.x * 0.5, coordinate.y * 0.5)
|
||||
obj = bpy.data.objects.new("Depth Surface Step", data)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = _add_cutout_modifier(obj, "DEPTH_SOLID")
|
||||
|
||||
points = np.empty((3, 3, 3), dtype=np.float32)
|
||||
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 (
|
||||
("Depth Image", depth_image),
|
||||
("Depth Scale", 1.0),
|
||||
("Uniform Scale", 1.0),
|
||||
("Reference Depth", 1.0),
|
||||
("Thickness", 0.0),
|
||||
("Smooth", 0),
|
||||
):
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(modifier.node_group, name),
|
||||
value,
|
||||
)
|
||||
return obj, modifier
|
||||
|
||||
|
||||
def _evaluated_topology(obj):
|
||||
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
result = evaluated.to_mesh()
|
||||
try:
|
||||
face_edges = {
|
||||
tuple(sorted((start, end)))
|
||||
for polygon in result.polygons
|
||||
for start, end in zip(
|
||||
polygon.vertices,
|
||||
(*polygon.vertices[1:], polygon.vertices[0]),
|
||||
)
|
||||
}
|
||||
mesh_edges = {
|
||||
tuple(sorted(edge.vertices))
|
||||
for edge in result.edges
|
||||
}
|
||||
face_vertices = {
|
||||
vertex
|
||||
for polygon in result.polygons
|
||||
for vertex in polygon.vertices
|
||||
}
|
||||
return (
|
||||
len(result.vertices),
|
||||
len(result.polygons),
|
||||
mesh_edges - face_edges,
|
||||
set(range(len(result.vertices))) - face_vertices,
|
||||
)
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
17
tests/support/materials.py
Normal file
17
tests/support/materials.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import bpy
|
||||
import pytest
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
from tools.nodes.groups.image_layer import build_image_layer_group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_layer(request):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
if getattr(request, "param", "definition") == "asset":
|
||||
with bpy.data.libraries.load(str(PROJECT_ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (_, target):
|
||||
target.node_groups = ["O Image Layer"]
|
||||
group = target.node_groups[0]
|
||||
else:
|
||||
group = build_image_layer_group()
|
||||
yield group
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
22
tests/support/nodes.py
Normal file
22
tests/support/nodes.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Shared assertions and asset selection for node tests."""
|
||||
from tools.nodes.groups.image_cutout import build_image_cutout_group
|
||||
from tools.nodes.groups.image_depth_cutout import build_image_depth_cutout_group
|
||||
from tools.nodes.groups.image_depth_balloon import build_image_depth_balloon_group
|
||||
|
||||
CUTOUT_BUILDERS = {
|
||||
"FLAT": build_image_cutout_group,
|
||||
"SOLID": build_image_cutout_group,
|
||||
"DEPTH_SOLID": build_image_depth_cutout_group,
|
||||
"DEPTH_BALLOON": build_image_depth_balloon_group,
|
||||
}
|
||||
|
||||
|
||||
def validate_node_structure(node_group):
|
||||
"""Check visible sockets, default titles, and paired execution endpoints."""
|
||||
nodes = [node for node in node_group.nodes if node.bl_idname not in {"NodeReroute", "NodeFrame"}]
|
||||
assert all(not node.label for node in nodes), node_group.name
|
||||
assert not any(node.bl_idname == "FunctionNodeInputString" for node in nodes), node_group.name
|
||||
assert all(socket.is_linked or socket.hide for node in node_group.nodes for socket in node.outputs), node_group.name
|
||||
for node in nodes:
|
||||
if node.bl_idname in {"GeometryNodeRepeatInput", "GeometryNodeSimulationInput", "GeometryNodeForeachGeometryElementInput"}:
|
||||
assert node.paired_output in node_group.nodes.values(), (node_group.name, node.name)
|
||||
4
tests/support/paths.py
Normal file
4
tests/support/paths.py
Normal file
@@ -0,0 +1,4 @@
|
||||
"""Resolve project paths independently of test directory depth."""
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
82
tests/support/planes.py
Normal file
82
tests/support/planes.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""Evaluate complete-image projection and rectangular surface topology."""
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = PROJECT_ROOT
|
||||
from tools.nodes.groups.image_plane import build_image_plane_group
|
||||
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
|
||||
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||
|
||||
|
||||
@pytest.fixture(params=("definition", "asset"))
|
||||
def surface(request):
|
||||
return create_surface(request.param, "DEPTH")
|
||||
|
||||
|
||||
@pytest.fixture(params=("definition", "asset"))
|
||||
def relief(request):
|
||||
return create_surface(request.param, "RELIEF")
|
||||
|
||||
|
||||
def create_surface(source, plane_type):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group_name = f"O Image {plane_type.title()} Plane"
|
||||
if source == "asset":
|
||||
with bpy.data.libraries.load(str(ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (library, target):
|
||||
target.node_groups = [group_name]
|
||||
group = target.node_groups[0]
|
||||
plane = group.nodes["Group"].node_tree
|
||||
else:
|
||||
plane = build_image_plane_group()
|
||||
builder = build_image_depth_plane_group if plane_type == "DEPTH" else build_image_relief_plane_group
|
||||
group = builder(plane)
|
||||
mesh = bpy.data.meshes.new("Rectangle")
|
||||
mesh.from_pydata([(-2, -1, 0), (2, -1, 0), (2, 1, 0), (-2, 1, 0)], [], [(0, 1, 2, 3)])
|
||||
material = bpy.data.materials.new("Image")
|
||||
mesh.materials.append(material)
|
||||
obj = bpy.data.objects.new("Surface", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new("Surface", "NODES")
|
||||
modifier.node_group = group
|
||||
image = bpy.data.images.new("Camera", width=16, height=8, float_buffer=True)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
yy, xx = np.mgrid[:8, :16]
|
||||
# Camera principal point is deliberately off center; Y points down.
|
||||
pixels = np.ones((8, 16, 4), dtype=np.float32)
|
||||
pixels[..., 0] = ((xx + 0.5) - 5) / 4
|
||||
pixels[..., 1] = (3 - (yy + 0.5)) / 4
|
||||
pixels[..., 2] = 2 + (xx + 0.5) / 16
|
||||
pixels[..., 3] = 1
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
inputs = {item.name: item for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
|
||||
def set_value(name, value):
|
||||
modifier[inputs[name].identifier] = value
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
for name, value in (("Subdivide", 2), ("Depth Image", image),
|
||||
("Uniform Scale", 0.5), ("Reference Depth", 1.2),
|
||||
("Depth Scale", 0.0), ("Thickness", 0.0)):
|
||||
set_value(name, value)
|
||||
return obj, set_value, plane, inputs, image
|
||||
|
||||
|
||||
def evaluated(obj):
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
return (
|
||||
np.array([v.co[:] for v in mesh.vertices]),
|
||||
[tuple(face.vertices) for face in mesh.polygons],
|
||||
np.array([value.uv[:] for value in mesh.uv_layers["UVMap"].data]) if mesh.uv_layers else np.empty((0, 2)),
|
||||
[material.name if material else None for material in mesh.materials],
|
||||
[face.material_index for face in mesh.polygons],
|
||||
[attr.name for attr in mesh.attributes],
|
||||
)
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
Reference in New Issue
Block a user