Add new functionality for image rectification and depth metadata handling

- Implemented a preview tool for rectifying images with interactive aspect controls in `preview.py`.
- Added tests for depth metadata loading to ensure consistent schema usage in `test_depth_metadata.py`.
- Developed comprehensive tests for cutout calibration processes in `test_cutout_calibration.py`, including stability checks and depth calibration.
- Created tests for frame compositing operations in `test_frame_compositing.py`, ensuring correct handling of depth and transparency.
- Introduced material slot inheritance functionality in `material_slots.py` to manage geometry material assignments.
- Built shared shader nodes for material processing in `shader.py`, including roughness and bump mapping setups.
This commit is contained in:
2026-09-12 19:20:06 +08:00
parent e8268504c2
commit 938d95db0f
175 changed files with 3812 additions and 6864 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -70,16 +70,12 @@ def test_camera_blur_preserves_rays_interior_protection_and_disconnected_surface
camera[valid, :2] / camera[valid, 2:3], atol=1e-6)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_cutout_fine_outline_blurs_depth_without_moving_projection(source):
def test_cutout_fine_outline_blurs_depth_without_moving_projection():
from tests.support.depth_surface import surface
from anyimage.operators.cutout_tool.fine_mesh import build_fine_mesh
from anyimage.operators.cutout_tool.object import load_cutout_node_group
obj, set_value = surface(step=0, resolution=8)
if source == "asset":
bpy.data.node_groups.remove(obj.modifiers[0].node_group, do_unlink=True)
obj.modifiers[0].node_group = load_cutout_node_group("DEPTH_SOLID")
yy, xx = np.mgrid[:32, :64]
alpha = ((xx - 32)**2 / 27**2 + (yy - 16)**2 / 13**2 < 1).astype(float)
points, faces, _, _ = build_fine_mesh(alpha, 3, .9)
@@ -111,11 +107,10 @@ def test_cutout_fine_outline_blurs_depth_without_moving_projection(source):
np.testing.assert_array_equal(evaluated(obj)[0], before)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_plane_validity_hole_blurs_depth_and_protects_rectangle(source):
def test_plane_validity_hole_blurs_depth_and_protects_rectangle():
from tests.support.planes import create_surface, evaluated as plane_mesh
obj, set_value, _, _, image = create_surface(source, "DEPTH")
obj, set_value, _, _, image = create_surface("DEPTH")
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
pixels[..., 2] = 2
pixels[2:6, 6:10, 3] = 0
@@ -136,17 +131,15 @@ def test_plane_validity_hole_blurs_depth_and_protects_rectangle(source):
np.testing.assert_array_equal(after[outer], before[outer])
np.testing.assert_allclose(after[:, :2] / (1.2 - after[:, 2:3]),
before[:, :2] / (1.2 - before[:, 2:3]), atol=1e-6)
set_value("Valid Only", False)
set_value("Depth Mask", False)
intact = plane_mesh(obj)[0]
set_value("Boundary Smooth", 0)
np.testing.assert_array_equal(plane_mesh(obj)[0], intact)
@pytest.mark.parametrize("source", ["definition", "asset"])
@pytest.mark.parametrize("region", ["seam", "pole"])
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(source, region):
def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(region):
from tests.tools.nodes.test_image_depth_panorama import panorama, evaluated as panorama_mesh
from tools.nodes.build import OUTPUT_PATH
yy, xx = np.mgrid[:64, :128]
distance = np.minimum(xx, 128 - xx)
@@ -154,11 +147,6 @@ def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(source, regi
spike = ((distance < 4) & (yy > 40) & (yy < 44)) if region == "seam" else ((yy > 50) & (xx < 8))
depth = np.where(spike, 10., 3.)
obj, set_value = panorama(depth, (~hole).astype(float), subdivide=5)
if source == "asset":
obj.modifiers[0].node_group.name = "Panorama definition"
with bpy.data.libraries.load(str(OUTPUT_PATH)) as (_, target):
target.node_groups = ["O Image Depth Panorama"]
obj.modifiers[0].node_group = target.node_groups[0]
set_value("Smooth Weight", 0)
before, faces, uv = panorama_mesh(obj)
set_value("Boundary Smooth", 12)
@@ -174,7 +162,7 @@ def test_panorama_blurs_opening_radially_and_keeps_invalid_distance(source, regi
interior = [i for i in range(len(before)) if i not in band]
assert interior
np.testing.assert_array_equal(after[interior], before[interior])
set_value("Valid Only", False)
set_value("Depth Mask", False)
near = panorama_mesh(obj)[0]
set_value("Invalid Distance", 250)
far = panorama_mesh(obj)[0]

View File

@@ -24,7 +24,7 @@ def diagonal_surface(triangles=False):
pixels[..., 2] = 1 + (xx > 18 + yy * 0.7) * 6
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
set_value("Depth Scale", 0)
return obj, set_value
@@ -85,16 +85,9 @@ def test_smoothing_reduces_stairs_without_leaving_two_ring_band(triangles):
np.testing.assert_array_equal(evaluated(obj)[0], original)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_smoothing_without_split_moves_original_boundary_only_within_band(source):
def test_smoothing_without_split_moves_original_boundary_only_within_band():
obj, set_value = diagonal_surface()
if source == "asset":
from anyimage.common.node import node_asset_path
with bpy.data.libraries.load(str(node_asset_path())) as (_, target):
target.node_groups = ["O Image Depth Cutout"]
obj.modifiers[0].node_group = target.node_groups[0]
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
original, faces = evaluated(obj)
set_value("Boundary Smooth", 20)
result, result_faces = evaluated(obj)
@@ -136,7 +129,7 @@ def test_panorama_smoothing_preserves_interior_and_uvs():
yy, xx = np.mgrid[:64, :128]
depth = np.where(xx > 35 + yy * 0.6, 8.0, 2.0)
obj, set_value = panorama(depth, subdivide=4)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
original, faces, uv = panorama_mesh(obj)
rim = boundary_neighbors(faces)
band = edge_band(faces, rim)
@@ -150,11 +143,10 @@ def test_panorama_smoothing_preserves_interior_and_uvs():
assert np.max(np.linalg.norm(result-original, axis=1)) > 1e-4
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness():
from tests.support.planes import create_surface, evaluated as plane_mesh
obj, set_value, _, inputs, image = create_surface(source, "DEPTH")
obj, set_value, _, inputs, image = create_surface("DEPTH")
assert inputs["Boundary Smooth"].default_value == 2
assert inputs["Boundary Smooth"].parent.name == "Options"
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
@@ -164,7 +156,7 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
image.update()
set_value("Subdivide", 5)
set_value("Depth Scale", 1)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
original, faces, uv, *_ = plane_mesh(obj)
set_value("Boundary Smooth", 4)
result, result_faces, result_uv, *_ = plane_mesh(obj)
@@ -175,76 +167,13 @@ def test_plane_edge_smoothing_preserves_uvs_and_closes_thickness(source):
assert_closed(plane_mesh(obj)[1])
def test_edge_iterations_reuse_boundary_topology_and_mapping():
from tools.nodes.arrangement.flow import origin_socket
from tools.nodes.arrangement.zones import node_zones
obj, _ = diagonal_surface()
group = obj.modifiers[0].node_group
entry, _, members = next(
zone for zone in node_zones(group)
if origin_socket(zone[0].inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth"
)
nearest = next(node for node in group.nodes if node.bl_idname == "GeometryNodeSampleNearest"
and any(link.to_node in members for link in node.outputs["Index"].links))
fixed_position = origin_socket(nearest.inputs["Sample Position"].links[0].from_socket).node
assert fixed_position.bl_idname == "GeometryNodeSampleIndex"
source = origin_socket(entry.inputs["Geometry"].links[0].from_socket)
assert origin_socket(fixed_position.inputs["Geometry"].links[0].from_socket) == source
assert fixed_position.domain == "POINT"
assert any(node.bl_idname == "GeometryNodeSeparateGeometry" for node in members)
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_every_split_node_enables_boundary_smoothing_by_default(source):
from tools.nodes.build import OUTPUT_PATH, build_node_groups
from tools.nodes.arrangement.flow import origin_socket
bpy.ops.wm.read_factory_settings(use_empty=True)
if source == "definition":
groups = build_node_groups()
else:
with bpy.data.libraries.load(str(OUTPUT_PATH)) as (library, target):
target.node_groups = list(library.node_groups)
groups = target.node_groups
split_groups = []
for group in groups:
inputs = {item.name: item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert "Edge Smooth" not in inputs
if "Split Threshold" not in inputs:
continue
split_groups.append(group.name)
smooth = inputs["Boundary Smooth"]
assert smooth.default_value == (5 if group.name == "O Image Depth Panorama" else 2)
assert (smooth.min_value, smooth.max_value) == (1 if "Outline Depth Fix" in inputs else 0, 20)
assert smooth.parent.name == "Options"
assert not {"Depth Blur Iterations", "Depth Band Rings", "Depth Blur Strength", "Skip Inactive Geometry"} & inputs.keys()
depth_blurs = [n for n in group.nodes if n.bl_idname == "GeometryNodeBlurAttribute"
and n.data_type == "FLOAT" and n.inputs["Iterations"].is_linked
and origin_socket(n.inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth"]
assert len(depth_blurs) == 1
if "Outline Depth Fix" in inputs:
assert inputs["Outline Depth Fix"].default_value == 16
assert origin_socket(depth_blurs[0].inputs["Weight"].links[0].from_socket).node.bl_idname == "GeometryNodeSampleIndex"
continue
depth_bypass = [n for n in group.nodes if n.bl_idname == "GeometryNodeSwitch"
and n.input_type == "FLOAT" and n.inputs["Switch"].is_linked
and origin_socket(n.inputs["Switch"].links[0].from_socket).name == "Boundary Smooth"]
assert len(depth_bypass) == 1
assert set(split_groups) == {
"O Image Depth Plane", "O Image Depth Cutout", "O Image Depth Panorama",
}
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
def test_validity_cut_smoothing_preserves_original_outline_and_interior():
from tests.support.planes import create_surface, evaluated as plane_mesh
obj, set_value, _, _, image = create_surface(source, "DEPTH")
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 5)
set_value("Split Threshold", 0)
set_value("Valid Only", False)
set_value("Depth Split", 0)
set_value("Depth Mask", False)
whole = plane_mesh(obj)[0]
low, high = whole.min(axis=0), whole.max(axis=0)
pixels = np.array(image.pixels[:], np.float32).reshape(8, 16, 4)
@@ -252,7 +181,7 @@ def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
pixels[..., 3] = xx < 5 + yy * 0.7
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Valid Only", True)
set_value("Depth Mask", True)
before, faces, uv, *_ = plane_mesh(obj)
boundary = boundary_neighbors(faces)
outer = np.any(np.isclose(before[:, :2], low[:2]) | np.isclose(before[:, :2], high[:2]), axis=1)
@@ -274,7 +203,7 @@ def test_validity_cut_smoothing_preserves_original_outline_and_interior(source):
set_value("Thickness", 0.03)
assert_closed(plane_mesh(obj)[1])
set_value("Thickness", 0)
set_value("Valid Only", False)
set_value("Depth Mask", False)
np.testing.assert_array_equal(plane_mesh(obj)[0], whole)
@@ -284,7 +213,7 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band():
yy, xx = np.mgrid[:64, :128]
alpha = ((xx-64)**2 + (yy-32)**2 > 15**2).astype(np.float32)
obj, set_value = panorama(np.full((64, 128), 3), alpha, subdivide=4)
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
before, faces, uv = panorama_mesh(obj)
band = edge_band(faces, boundary_neighbors(faces))
outside = [i for i in range(len(before)) if i not in band]
@@ -294,45 +223,3 @@ def test_panorama_validity_cut_smoothing_stays_in_two_ring_band():
np.testing.assert_array_equal(result_uv, uv)
np.testing.assert_array_equal(after[outside], before[outside])
assert np.max(np.linalg.norm(after-before, axis=1)) > 1e-4
@pytest.mark.parametrize("source", ["definition", "asset"])
@pytest.mark.parametrize("kind", ["cutout", "plane", "panorama"])
@pytest.mark.parametrize("iterations,weight", [(0, 1.0), (8, 1.0), (8, 0.0)])
def test_boundary_loop_bypass_uses_iterations_only(source, kind, iterations, weight):
from anyimage.common.node import node_asset_path
from tests.support.planes import create_surface
from tests.tools.nodes.test_image_depth_panorama import panorama
from tools.nodes.arrangement.flow import origin_socket
if kind == "cutout":
obj, set_value = surface(resolution=4)
elif kind == "plane":
obj, set_value, *_ = create_surface("definition", "DEPTH")
else:
obj, set_value = panorama(subdivide=2)
if source == "asset":
with bpy.data.libraries.load(str(node_asset_path())) as (_, target):
target.node_groups = [obj.modifiers[0].node_group.name]
obj.modifiers[0].node_group = target.node_groups[0].copy()
set_value("Split Threshold", 0)
set_value("Smooth Weight", weight)
set_value("Boundary Smooth", iterations)
group = obj.modifiers[0].node_group
entry = next(n for n in group.nodes if n.bl_idname == "GeometryNodeRepeatInput"
and origin_socket(n.inputs["Iterations"].links[0].from_socket).name == "Boundary Smooth")
bypass = next(n for n in group.nodes if n.bl_idname == "GeometryNodeSwitch"
and n.input_type == "GEOMETRY" and n.inputs["True"].is_linked
and origin_socket(n.inputs["True"].links[0].from_socket).node == entry.paired_output)
control = origin_socket(bypass.inputs["Switch"].links[0].from_socket)
assert control.name == "Boundary Smooth"
assert control.identifier == origin_socket(entry.inputs["Iterations"].links[0].from_socket).identifier
assert origin_socket(bypass.inputs["False"].links[0].from_socket) == origin_socket(entry.inputs["Geometry"].links[0].from_socket)
original, faces = evaluated(obj)
group.links.remove(bypass.inputs["Switch"].links[0])
bypass.inputs["Switch"].default_value = True
obj.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
unskipped, unskipped_faces = evaluated(obj)
np.testing.assert_array_equal(original, unskipped)
assert faces == unskipped_faces

View File

@@ -70,69 +70,31 @@ def test_blender_path_reports_missing_configuration(tmp_path):
launcher.blender_path()
def test_launcher_runs_asset_build_and_validations_before_tests(monkeypatch, tmp_path):
launcher = load_launcher()
executable = tmp_path / "blender"
executable.touch()
calls = []
monkeypatch.setattr(launcher, "blender_path", lambda _: executable)
monkeypatch.setattr(launcher, "run", calls.append)
monkeypatch.setattr("sys.argv", ["node-group", "build"])
launcher.main()
assert len(calls) == 3
assert "runpy.run_module('tools.nodes.build', run_name='__main__')" in calls[0][-1]
assert "runpy.run_module('tools.nodes.check', run_name='__main__')" in calls[1][-1]
assert all(command[1:6] == ("--factory-startup", "--background", "--python-exit-code", "1", "--python-expr") for command in calls[:-1])
assert calls[-1][1:] == ("-m", "pytest")
@pytest.mark.parametrize("skip_tests", [False, True])
def test_check_validates_without_building(monkeypatch, skip_tests):
launcher = load_launcher()
calls = []
monkeypatch.setattr(launcher, "blender_path", lambda _: "blender")
monkeypatch.setattr(launcher, "run", calls.append)
monkeypatch.setattr("sys.argv", ["node-group", "check", *(["--skip-tests"] if skip_tests else [])])
launcher.main()
assert len(calls) == (1 if skip_tests else 2)
assert "tools.nodes.check" in calls[0][-1]
assert "tools.nodes.build" not in calls[0][-1]
def test_build_stops_after_blender_failure(monkeypatch):
launcher = load_launcher()
monkeypatch.setattr(launcher, "blender_path", lambda _: "blender")
monkeypatch.setattr("sys.argv", ["node-group", "build"])
with patch.object(launcher, "run", side_effect=RuntimeError("failed")) as run:
with pytest.raises(RuntimeError, match="failed"):
launcher.main()
assert run.call_count == 1
@pytest.mark.parametrize("arguments,opens", [([], True), (["--no-open"], False), (["--fragment"], False)])
def test_preview_passes_isolated_arguments_and_opens_result(monkeypatch, tmp_path, arguments, opens):
@pytest.mark.parametrize("arguments,modules,run_tests", [
(["build"], ("tools.nodes.build", "tools.nodes.check"), True),
(["check", "--skip-tests"], ("tools.nodes.check",), False),
(["preview", "--fragment"], ("tools.nodes.preview.export",), False),
])
def test_commands_dispatch_requested_work(monkeypatch, tmp_path, arguments, modules, run_tests):
launcher = load_launcher()
output = tmp_path / "node groups.html"
calls = []
command = ["node-group", *arguments]
if arguments[0] == "preview":
command += ["--output", str(output)]
monkeypatch.setattr("sys.argv", command)
monkeypatch.setattr(launcher, "blender_path", lambda _: "blender")
monkeypatch.setattr(launcher, "run", calls.append)
monkeypatch.setattr("sys.argv", ["node-group", "preview", "--output", str(output), *arguments])
with patch.object(launcher.webbrowser, "open") as open_browser:
with (
patch.object(launcher, "run_blender_modules") as blender,
patch.object(launcher, "run") as run,
patch.object(launcher.webbrowser, "open") as open_browser,
):
launcher.main()
assert len(calls) == 1
expression = calls[0][-1]
import ast
assignment = next(
node for node in ast.parse(expression).body
if isinstance(node, ast.Assign) and ast.unparse(node.targets[0]) == "sys.argv"
)
assert ast.literal_eval(assignment.value) == [
"tools.nodes.preview.export", "--output", str(output),
*(["--fragment"] if "--fragment" in arguments else []),
]
if opens:
open_browser.assert_called_once_with(output.as_uri())
else:
if arguments[0] == "preview":
blender.assert_called_once_with("blender", modules, ["--output", str(output), "--fragment"])
open_browser.assert_not_called()
else:
blender.assert_called_once_with("blender", modules)
if run_tests:
run.assert_called_once_with((launcher.sys.executable, "-m", "pytest"))
else:
run.assert_not_called()

View File

@@ -37,7 +37,7 @@ def test_boundary_triangle_cleanup_is_one_pass():
assert all(len(f) == 3 for f in faces)
@pytest.mark.parametrize("iterations", [0, 5, 12, 20])
@pytest.mark.parametrize("iterations", [0, 20])
def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
obj, set_value = diagonal_surface(triangles=True)
original, faces = evaluated(obj)
@@ -50,8 +50,7 @@ def test_cleaned_sawtooth_smoothing_preserves_face_orientation(iterations):
assert np.all(np.sum(normals(original)*normals(points), axis=1) > 0)
@pytest.mark.parametrize("resolution", [12, 24])
@pytest.mark.parametrize("aspect", [.5, 2])
@pytest.mark.parametrize("resolution,aspect", [(12, .5), (24, 2)])
def test_split_profile_tapers_within_boundary_band(resolution, aspect):
obj, set_value = surface(step=6, resolution=resolution)
for vertex in obj.data.vertices:
@@ -65,7 +64,7 @@ def test_split_profile_tapers_within_boundary_band(resolution, aspect):
"value", np.full(len(obj.data.vertices), .3, dtype=np.float32))
set_value("Depth Scale", 0)
np.testing.assert_allclose(profile_values(obj), .3)
set_value("Split Threshold", .1)
set_value("Depth Split", .1)
points, faces = evaluated(obj)
boundary = boundary_neighbors(faces)
cut = [i for i in boundary if 0 < points[i,1] < 1 and .5*aspect < points[i,0] < 1.5*aspect]
@@ -78,7 +77,7 @@ def test_split_profile_tapers_within_boundary_band(resolution, aspect):
np.testing.assert_allclose(values[outside], .3)
assert np.isfinite(values).all()
assert values.max() == pytest.approx(.3)
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
np.testing.assert_allclose(profile_values(obj), .3)
@@ -87,13 +86,13 @@ def test_split_profile_tapers_within_boundary_band(resolution, aspect):
def test_normal_blend_converges_to_raw_and_reaches_full_smoothing(mode, reference):
obj, set_value = surface(resolution=2)
group = obj.modifiers[0].node_group
# The production interface renames the uniform socket after building.
# The production interface renames the Shell thickness socket after building.
socket = next(s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"
and s.name == "Thickness" and s.subtype == "DISTANCE")
socket.name = "Uniform Thickness"
uniform = group.nodes.new("FunctionNodeInputBool")
uniform.boolean = bool(mode)
weight = build_normal_weight(group, uniform.outputs[0])
socket.name = "Shell Thickness"
is_shell = group.nodes.new("FunctionNodeInputBool")
is_shell.boolean = bool(mode)
weight = build_normal_weight(group, is_shell.outputs[0])
raw = group.nodes.new("ShaderNodeCombineXYZ")
raw.inputs["Z"].default_value = 1
blurred = group.nodes.new("ShaderNodeCombineXYZ")

View File

@@ -4,67 +4,36 @@ import bpy
import numpy as np
import pytest
from tools.nodes.groups.image_cutout import build_image_cutout_group
from tests.support.nodes import CUTOUT_BUILDERS
from tools.nodes.arrangement.flow import origin_socket
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.common.object import modifier_input_identifier, set_modifier_input
from tests.support.nodes import validate_node_structure
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_cutout_interfaces_and_thickness_modes(source):
@pytest.mark.parametrize("source", ("definition", "asset"))
@pytest.mark.parametrize("shape", ("SOLID", "DEPTH_SOLID", "DEPTH_BALLOON"))
def test_depth_axis_is_visible_in_options(shape, source):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = CUTOUT_BUILDERS[shape]() if source == "definition" else load_cutout_node_group(shape)
inputs = {item.name: item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
axis = inputs["Depth Axis"]
assert axis.parent.name == "Options"
assert axis.menu_expanded
assert not axis.hide_in_modifier
assert axis.default_value == "-Z"
assert "Inward Axis" not in inputs
options = [item.name for item in inputs.values() if item.parent == axis.parent]
predecessor = "Smooth Weight" if shape == "SOLID" else "Reference Depth"
assert options[options.index(predecessor) + 1] == "Depth Axis"
if shape == "SOLID":
assert not any(item.item_type == "PANEL" and item.name == "Data" for item in group.interface.items_tree)
def test_cutout_thickness_modes():
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {shape: (CUTOUT_BUILDERS[shape]() if source == "definition" else load_cutout_node_group(shape)) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
assert groups["FLAT"] == groups["SOLID"]
assert len(set(groups.values())) == 3
for group in set(groups.values()):
validate_node_structure(group)
revision = 23 if group == groups["DEPTH_SOLID"] else 12 if group == groups["DEPTH_BALLOON"] else 11
assert group.is_modifier and group.get("anyimage_asset_revision") == revision
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
inputs = {item.name: item for item in group.interface.items_tree if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert "Shape" not in inputs and "Rim Thickness" not in inputs
assert next(s for s in group.interface.items_tree if s.name == "Thickness").default_value == 1
assert all(item.structure_type == "SINGLE" for item in inputs.values())
if "Mode" in inputs:
assert inputs["Thickness"].subtype == "DISTANCE"
assert inputs["Mode"].default_value == "Balloon"
if "Double Sided" in inputs:
assert inputs["Double Sided"].default_value
if group == groups["DEPTH_SOLID"]:
assert np.isclose(inputs["Split Threshold"].default_value, 0.1)
assert not {"Edge Round", "Normal Smooth"} & inputs.keys()
normal_blurs = [n for n in group.nodes if n.bl_idname == "GeometryNodeBlurAttribute" and n.data_type == "FLOAT_VECTOR"
and not n.inputs["Iterations"].is_linked and n.inputs["Iterations"].default_value == 100]
assert len(normal_blurs) == 2
assert inputs["Front Inflation"].default_value == 0
assert inputs["Front Inflation"].subtype == "FACTOR"
assert (inputs["Front Inflation"].min_value, inputs["Front Inflation"].max_value) == (0, 1)
assert inputs["Front Inflation"].parent.name == "Options"
assert not set(inputs) & {"Edge Round Range", "Back Rim Range", "Normal Influence", "Edge Round Amount", "Back Rim Fullness"}
options = [s.name for s in inputs.values() if s.parent and s.parent.name == "Options"]
expected = ["Cleanup Threshold"]
if group == groups["DEPTH_SOLID"]:
expected += ["Smooth Weight", "Reference Depth", "Boundary Smooth", "Outline Depth Fix", "Front Inflation"]
assert inputs["Outline Depth Fix"].default_value == 16
assert not set(inputs) & {"Ratio Lower", "Ratio Upper", "Weight Blur Iterations", "Weight Update Rounds", "Depth Steps Per Round"}
elif group == groups["DEPTH_BALLOON"]:
expected += ["Smooth Weight", "Reference Depth"]
if group not in (groups["DEPTH_SOLID"], groups["DEPTH_BALLOON"]):
expected += ["Smooth Weight"]
if group == groups["DEPTH_BALLOON"]:
expected += ["Depth Direction"]
assert options == expected, (group.name, options)
assert inputs["Smooth"].socket_type == "NodeSocketInt" and inputs["Smooth"].default_value == 0
assert inputs["Smooth Weight"].default_value == 1
incoming = [link for link in group.links
if link.to_node.bl_idname != "NodeReroute"
and origin_socket(link.from_socket).node.bl_idname == "NodeGroupInput"
and origin_socket(link.from_socket).name == "Geometry"]
assert len(incoming) == 1 and incoming[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
mesh = bpy.data.meshes.new("Profile")
mesh.from_pydata([(-1,-1,0), (1,-1,0), (1,1,0), (-1,1,0), (0,0,0)], [], [(0,1,4), (1,2,4), (2,3,4), (3,0,4)])
profile = mesh.attributes.new("o_balloon", "FLOAT", "POINT")
@@ -72,7 +41,7 @@ def test_cutout_interfaces_and_thickness_modes(source):
obj = bpy.data.objects.new("Profile", mesh)
bpy.context.collection.objects.link(obj)
modifier = obj.modifiers.new("Cutout", "NODES")
modifier.node_group = groups["SOLID"]
modifier.node_group = build_image_cutout_group()
for mode, thickness in ((0, 0), (0, 1), (1, 0), (1, 0.4)):
for name, value in (("Mode", mode), ("Thickness", thickness)):
subtype = ("DISTANCE" if mode else "NONE") if name == "Thickness" else None
@@ -88,8 +57,8 @@ def test_cutout_interfaces_and_thickness_modes(source):
assert len(result.polygons) == 4 and np.max(abs(z)) < 1e-7
assert all(abs(item.value - 1) < 1e-7 for item in result.attributes["o_normal_scale"].data)
else:
radius = 0.6 if mode == 0 else 0.2
assert np.isclose(z.min(), -radius) and np.isclose(z.max(), radius)
back, front = (-0.6, 0.6) if mode == 0 else (-thickness, 0.0)
assert np.isclose(z.min(), back) and np.isclose(z.max(), front)
uses = Counter(tuple(sorted((a,b))) for face in result.polygons for a,b in zip(face.vertices, (*face.vertices[1:],face.vertices[0])))
assert set(uses.values()) == {2}
finally:

View File

@@ -35,7 +35,7 @@ def set_test_structure(monkeypatch, value):
monkeypatch.setattr(image_depth_cutout, "build_front_fields", fields)
def test_uniform_thickness_follows_the_tilted_front_normal():
def test_shell_thickness_extends_behind_the_tilted_front():
from scipy.spatial import cKDTree
from tests.support.depth_surface import surface, evaluated, assert_closed
obj, set_value = surface(step=0, resolution=4)
@@ -48,6 +48,7 @@ def test_uniform_thickness_follows_the_tilted_front_normal():
bpy.context.view_layer.update()
front, _ = evaluated(obj)
set_value("Thickness", 0.2)
set_value("Front Inflation", 0.5)
points, faces = evaluated(obj)
expected = front - 0.2 * np.array((0.4, 0, 1)) / np.sqrt(1.16)
assert cKDTree(points).query(expected)[0].max() < 1e-5
@@ -140,12 +141,12 @@ def test_depth_cutout_normal_map_only_affects_front_faces():
evaluated.to_mesh_clear()
@pytest.mark.parametrize("structure", [0, 1, 4])
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
@pytest.mark.parametrize("inflation", [0.0, 0.0001, 0.5, 1.0])
def test_uniform_front_inflation_scales_with_distance(thickness, structure, inflation, monkeypatch):
@pytest.mark.parametrize("thickness,inflation", [
(0.0, 1.0), (0.02, 0.0), (0.02, 0.0001),
(0.02, 0.5), (0.1, 0.5), (0.1, 1.0),
])
def test_shell_ignores_front_inflation(thickness, inflation):
from tests.support.depth_surface import surface, evaluated, assert_closed
set_test_structure(monkeypatch, structure)
obj, set_value = surface(step=0, resolution=10)
xy = np.array([v.co[:2] for v in obj.data.vertices])
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
@@ -157,14 +158,9 @@ def test_uniform_front_inflation_scales_with_distance(thickness, structure, infl
set_value("Front Inflation", inflation)
inflated, inflated_faces = evaluated(obj)
assert faces == inflated_faces
delta = np.linalg.norm(inflated - baseline, axis=1)
front = np.isclose(baseline[:, 2], baseline[:, 2].max(), atol=1e-6)
np.testing.assert_allclose(delta[front], thickness * inflation, atol=2e-6)
np.testing.assert_allclose(delta[~front], 0, atol=2e-6)
np.testing.assert_array_equal(inflated, baseline)
if thickness:
assert_closed(inflated_faces)
else:
np.testing.assert_array_equal(inflated, baseline)
def test_balloon_suppression_is_stronger_without_a_startup_threshold(monkeypatch):

View File

@@ -6,8 +6,6 @@ import numpy as np
import pytest
from tests.support.planes import surface, relief, evaluated
from tests.support.nodes import validate_node_structure
from tools.nodes.arrangement.flow import origin_socket
@pytest.mark.parametrize("subdivide", [0, 1, 3])
@@ -59,7 +57,7 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness):
set_value("Depth Scale", 1.0)
front = evaluated(obj)[0]
set_value("Thickness", thickness)
points, faces, uv, materials, indices, attributes = evaluated(obj)
points, faces, uv, materials, indices, _ = evaluated(obj)
assert len(points) == 2 * len(front)
edges = Counter(tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0])))
assert set(edges.values()) == {2}
@@ -70,7 +68,6 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness):
assert cKDTree(points).query(expected)[0].max() < 1e-6
assert np.isfinite(points).all() and np.isfinite(uv).all()
assert materials[0] == "Image" and set(indices) == {0}
assert not any(name.startswith("o_depth_") for name in attributes)
@pytest.mark.parametrize("valid_only", [False, True])
@@ -84,16 +81,15 @@ def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Scale", 1.0)
set_value("Valid Only", valid_only)
set_value("Depth Mask", valid_only)
base = evaluated(obj)
set_value("Split Threshold", 1.0)
points, faces, uv, materials, indices, attributes = evaluated(obj)
set_value("Depth Split", 1.0)
points, faces, uv, materials, indices, _ = evaluated(obj)
assert len(points) > len(base[0])
assert len(faces) == len(base[1])
assert component_count(faces) > component_count(base[1])
np.testing.assert_allclose(uv, base[2])
assert (materials, indices) == base[3:5]
assert not any(name.startswith("o_depth_") for name in attributes)
set_value("Thickness", 0.2)
shell_points, shell_faces, shell_uv, *_ = evaluated(obj)
assert len(shell_points) == 2 * len(points)
@@ -102,7 +98,7 @@ def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
assert set(edges.values()) == {2}
assert np.isfinite(shell_points).all() and np.isfinite(shell_uv).all()
set_value("Thickness", 0.0)
set_value("Split Threshold", 0.0)
set_value("Depth Split", 0.0)
restored = evaluated(obj)
np.testing.assert_allclose(restored[0], base[0])
assert restored[1] == base[1]
@@ -197,35 +193,6 @@ def test_curved_shell_smooths_normals_without_shrinking_thickness(surface):
assert roughness(normals) < roughness(raw_normals)
def test_surface_interface_and_node_structure(surface):
obj, _, _, inputs, _ = surface
expected = ["Geometry", "Subdivide", "Thickness", "Depth Scale", "Split Threshold", "Smooth", "Valid Only",
"Smooth Weight", "Reference Depth", "Boundary Smooth", "Validity Threshold", "Uniform Scale", "Depth Image"]
assert list(inputs) == expected
assert inputs["Split Threshold"].default_value == pytest.approx(0.1)
assert inputs["Split Threshold"].min_value == 0
assert inputs["Split Threshold"].max_value == 1
assert inputs["Split Threshold"].subtype == "FACTOR"
assert inputs["Split Threshold"].parent == inputs["Smooth"].parent
assert inputs["Split Threshold"].parent.name != "Options"
assert inputs["Validity Threshold"].default_value == pytest.approx(0.9)
assert inputs["Validity Threshold"].parent.name == "Options"
assert inputs["Validity Threshold"].subtype == "FACTOR"
assert inputs["Valid Only"].default_value is True
assert inputs["Smooth"].default_value == 0
assert inputs["Smooth"].min_value == 0 and inputs["Smooth"].max_value == 20
assert inputs["Smooth Weight"].default_value == 1
assert inputs["Smooth Weight"].subtype == "FACTOR"
assert inputs["Smooth Weight"].parent.name == "Options"
assert not {"Normal Smooth", "Cleanup Threshold"} & inputs.keys()
assert inputs["Thickness"].default_value == 0
assert inputs["Subdivide"].default_value == 6
assert inputs["Depth Scale"].default_value == 1
assert all(item.structure_type == "SINGLE" for item in inputs.values())
assert inputs["Thickness"].subtype == inputs["Reference Depth"].subtype == "DISTANCE"
assert inputs["Uniform Scale"].hide_in_modifier and inputs["Depth Image"].hide_in_modifier
validate_node_structure(obj.modifiers[0].node_group)
@pytest.mark.parametrize("thickness", [0.0, 0.4])
@pytest.mark.parametrize("direction", [(0.2, -0.1, 1), (-0.2, 0.1, 1), (0.4, -0.2, 2), (0, 0, 0), (1, 0, 0)])
@@ -278,30 +245,13 @@ def test_relief_smooth_preserves_topology_uv_and_materials(relief, thickness):
np.testing.assert_allclose(evaluated(obj)[0], base[0], atol=1e-6)
def test_relief_interface_and_node_structure(relief):
obj, _, _, inputs, _ = relief
assert list(inputs) == ["Geometry", "Subdivide", "Thickness", "Depth Scale", "Smooth",
"Reference Depth", "Smooth Weight", "Depth Direction", "Uniform Scale", "Depth Image"]
assert inputs["Smooth"].default_value == 0
assert (inputs["Smooth"].min_value, inputs["Smooth"].max_value) == (0, 20)
assert inputs["Smooth Weight"].default_value == 1
assert inputs["Smooth Weight"].subtype == "FACTOR"
assert tuple(inputs["Depth Direction"].default_value) == (0, 0, 1)
assert inputs["Depth Direction"].subtype == "DIRECTION"
assert inputs["Thickness"].default_value == 0.5
assert inputs["Subdivide"].default_value == 6
assert inputs["Depth Scale"].default_value == 0.5
assert all(item.structure_type == "SINGLE" for item in inputs.values())
assert inputs["Thickness"].subtype == inputs["Reference Depth"].subtype == "DISTANCE"
assert inputs["Uniform Scale"].hide_in_modifier and inputs["Depth Image"].hide_in_modifier
validate_node_structure(obj.modifiers[0].node_group)
@pytest.mark.parametrize("threshold", [0.0, 0.4, 0.9, 1.0])
@pytest.mark.parametrize("pattern", ["valid", "invalid", "partial", "half", "stripe"])
@pytest.mark.parametrize("pattern,threshold", [
("valid", 1.0), ("invalid", 0.0), ("invalid", 0.9),
("partial", 0.4), ("partial", 0.9), ("half", 0.9), ("stripe", 0.9),
])
def test_valid_only_deletes_invalid_points_and_wire_edges(surface, pattern, threshold):
obj, set_value, _, inputs, _ = surface
set_value("Validity Threshold", threshold)
set_value("Mask Threshold", threshold)
depth_image = obj.modifiers[0][inputs["Depth Image"].identifier]
pixels = np.ones((8, 16, 4), dtype=np.float32)
alpha = np.ones(16, dtype=np.float32)
@@ -319,18 +269,18 @@ def test_valid_only_deletes_invalid_points_and_wire_edges(surface, pattern, thre
depth_image.update()
for subdivisions in (0, 2, 5):
set_value("Subdivide", subdivisions)
set_value("Valid Only", False)
set_value("Depth Mask", False)
base = evaluated(obj)
u = (base[0][:, 0] + 2) / 4
sampled = np.interp(u * 16 - 0.5, np.arange(16), alpha)
expected = [face for face in base[1] if np.all(sampled[list(face)] >= threshold)]
set_value("Valid Only", True)
set_value("Depth Mask", True)
culled = evaluated(obj)
assert len(culled[1]) == len(expected)
expected_points = {tuple(base[0][index]) for face in expected for index in face}
assert {tuple(point) for point in culled[0]} == expected_points
assert np.isfinite(culled[2]).all()
set_value("Valid Only", False)
set_value("Depth Mask", False)
np.testing.assert_allclose(evaluated(obj)[0], base[0])
@@ -351,26 +301,6 @@ def test_valid_only_precedes_closed_shell_generation(surface):
assert np.isfinite(points).all() and np.isfinite(uv).all()
def test_validity_graph_matches_reference_domains_and_connections(surface):
obj, _, _, _, _ = surface
group = obj.modifiers[0].node_group
delete = next(node for node in group.nodes if node.bl_idname == "GeometryNodeDeleteGeometry")
assert (delete.domain, delete.mode) == ("POINT", "ALL")
compare = origin_socket(delete.inputs["Selection"].links[0].from_socket).node
assert (compare.bl_idname, compare.data_type, compare.operation) == ("FunctionNodeCompare", "FLOAT", "LESS_THAN")
assert origin_socket(compare.inputs["B"].links[0].from_socket).name == "Validity Threshold"
alpha = origin_socket(compare.inputs["A"].links[0].from_socket)
assert alpha.name == "Alpha" and alpha.node.bl_idname == "FunctionNodeSeparateColor"
color = origin_socket(alpha.node.inputs["Color"].links[0].from_socket).node
assert origin_socket(color.inputs["Image"].links[0].from_socket).name == "Depth Image"
consumers = [link for link in group.links if link.to_node.bl_idname != "NodeReroute"
and origin_socket(link.from_socket) == color.outputs["Color"]]
assert any(link.to_node == alpha.node for link in consumers)
protected = origin_socket(delete.inputs["Geometry"].links[0].from_socket).node
assert protected.bl_idname == "GeometryNodeGroup"
assert protected.node_tree.name == "O Image Plane"
@pytest.mark.parametrize("thickness", [0.0, 0.2])
def test_depth_smooth_changes_positions_and_preserves_topology(surface, thickness):
obj, set_value, _, _, depth_image = surface
@@ -394,21 +324,3 @@ def test_depth_smooth_changes_positions_and_preserves_topology(surface, thicknes
set_value("Smooth Weight", 1.0)
set_value("Smooth", 0)
np.testing.assert_allclose(evaluated(obj)[0], base[0], atol=1e-6)
@pytest.mark.parametrize("kind", ["DEPTH", "RELIEF"])
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_plane_interface_panels(source, kind):
from tests.support.planes import create_surface
obj, _, _, _, _ = create_surface(source, kind)
node_group = obj.modifiers[0].node_group
panels = [item for item in node_group.interface.items_tree if item.item_type == "PANEL"]
assert [item.name for item in panels] == ["Options", "Data"]
controls = ["Reference Depth"]
if kind == "DEPTH":
controls = ["Smooth Weight", "Reference Depth", "Boundary Smooth", "Validity Threshold"]
else:
controls += ["Smooth Weight", "Depth Direction"]
assert [item.name for item in panels[0].interface_items] == controls
assert [item.name for item in panels[1].interface_items] == ["Uniform Scale", "Depth Image"]
assert all(panel.default_closed for panel in panels)

View File

@@ -5,9 +5,6 @@ import numpy as np
import pytest
from tools.nodes.common.depth_surface import _remove_strip_faces
from tools.nodes.groups.image_depth_cutout import build_image_depth_cutout_group
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
from tools.nodes.groups.image_plane import build_image_plane_group
def cleaned_faces(vertices, faces, *, triangles=False):
@@ -122,30 +119,13 @@ def test_triangle_cleanup_marks_both_faces_without_recursive_peeling():
assert cleaned_faces(vertices, faces, triangles=True) == {0, 2, 3}
@pytest.mark.parametrize("kind", ["plane", "cutout"])
def test_split_input_precedes_smooth_without_cleanup_switch(kind):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = (build_image_depth_plane_group(build_image_plane_group()) if kind == "plane"
else build_image_depth_cutout_group())
inputs = [s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"]
names = [s.name for s in inputs]
assert names.count("Split Threshold") == 1
assert names[names.index("Smooth") - 1] == "Split Threshold"
split = inputs[names.index("Split Threshold")]
assert split.parent == inputs[names.index("Smooth")].parent
assert split.parent.name != "Options"
assert split.default_value == pytest.approx(0.1)
assert (split.min_value, split.max_value, split.subtype) == (0, 1, "FACTOR")
assert "Remove Strip Faces" not in names
@pytest.mark.parametrize("kind", ["plane", "cutout"])
def test_strips_are_removed_before_projection_and_closed_thickness(kind):
from tests.support.depth_surface import surface, evaluated, assert_closed, component_count
from tests.support.planes import create_surface
if kind == "plane":
obj, set_value, _, _, image = create_surface("definition", "DEPTH")
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 1)
else:
obj, set_value = surface(step=0.0)
@@ -159,7 +139,7 @@ def test_strips_are_removed_before_projection_and_closed_thickness(kind):
set_value("Depth Scale", 1.0)
set_value("Reference Depth", 11.0)
assert len(evaluated(obj)[1]) == 8
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
vertices, faces = evaluated(obj)
assert len(faces) == 4 and len(vertices) == 9
assert component_count(faces) == 1
@@ -183,18 +163,18 @@ def test_zero_split_bypasses_cleanup_and_positive_split_can_empty_mesh(kind):
from tests.support.planes import create_surface
if kind == "plane":
obj, set_value, _, _, _ = create_surface("definition", "DEPTH")
obj, set_value, _, _, _ = create_surface("DEPTH")
set_value("Subdivide", 0)
else:
obj, set_value = surface(step=0.0, resolution=1)
original, faces = evaluated(obj)
assert len(faces) == 2
set_value("Split Threshold", 0.001)
set_value("Depth Split", 0.001)
assert not evaluated(obj)[1]
set_value("Thickness", 0.2)
assert not evaluated(obj)[1]
set_value("Thickness", 0.0)
set_value("Split Threshold", 0.0)
set_value("Depth Split", 0.0)
restored, restored_faces = evaluated(obj)
assert restored_faces == faces
np.testing.assert_array_equal(restored, original)
@@ -212,7 +192,7 @@ def test_cutout_removes_an_all_boundary_triangle_strip():
mesh.free()
# Every vertex lies on the boundary, even before any depth separation.
for split in (0.0, 0.001, 0.0):
set_value("Split Threshold", split)
set_value("Depth Split", split)
for thickness in (0.0, 0.2):
set_value("Thickness", thickness)
points, faces = evaluated(obj)

View File

@@ -7,7 +7,6 @@ import numpy as np
import pytest
from tests.support.nodes import CUTOUT_BUILDERS
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
@@ -20,7 +19,7 @@ def test_shell_walls_directly_connect_caps(mode, split):
obj.data.attributes.new("o_balloon", "FLOAT", "POINT").data.foreach_set(
"value", np.full(len(obj.data.vertices), 0.3)
)
set_value("Split Threshold", split)
set_value("Depth Split", split)
front, faces = evaluated(obj)
edges = Counter(tuple(sorted((a, b))) for face in faces for a, b in zip(face, (*face[1:], face[0])))
boundary_count = sum(count == 1 for count in edges.values())
@@ -51,7 +50,7 @@ def test_shell_walls_directly_connect_caps(mode, split):
@pytest.mark.parametrize("split", [0.0, 0.5])
def test_surface_caps_and_segmented_walls_are_connected(thickness, split):
obj, set_value = surface()
set_value("Split Threshold", split)
set_value("Depth Split", split)
set_value("Thickness", thickness)
coords, faces = evaluated(obj)
assert np.isfinite(coords).all()
@@ -69,7 +68,7 @@ def test_split_mapping_and_uniform_scale(size):
# The normalized ray jump crosses the threshold only at full strength.
counts = []
for split in (0.0, 0.25, 0.5, 1.0):
set_value("Split Threshold", split)
set_value("Depth Split", split)
coords, faces = evaluated(obj)
counts.append(len(coords))
assert len(faces) == 8
@@ -82,7 +81,7 @@ def test_split_mapping_and_uniform_scale(size):
def test_midpoint_selects_relative_ray_jump_and_preserves_faces():
obj, set_value = surface(step=4.0)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
coords, faces = evaluated(obj)
assert len(coords) == 18 and len(faces) == 8
assert set(np.round(coords[:, 2], 4)) == {2.0, -2.0}
@@ -92,7 +91,7 @@ def test_cleanup_keeps_split_parts_of_a_large_source_island():
obj, set_value = surface(wrapper=True)
set_value("Cleanup Threshold", 110.0)
assert len(evaluated(obj)[1]) == 8
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
assert len(evaluated(obj)[1]) == 8
set_value("Thickness", 0.2)
assert_closed(evaluated(obj)[1])
@@ -103,7 +102,7 @@ def test_cleanup_keeps_split_parts_of_a_large_source_island():
@pytest.mark.parametrize("depth_scale", [0.0, 0.25, 1.0, 2.0, 4.0])
def test_split_topology_uses_depth_before_display_scaling(depth_scale):
obj, set_value = surface(step=4.0)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
original, original_faces = evaluated(obj)
set_value("Depth Scale", depth_scale)
coords, faces = evaluated(obj)
@@ -112,18 +111,9 @@ def test_split_topology_uses_depth_before_display_scaling(depth_scale):
np.testing.assert_allclose(coords[:, 2], original[:, 2] * depth_scale, atol=1e-6)
@pytest.mark.parametrize("shape", ["SOLID", "DEPTH_SOLID", "DEPTH_BALLOON"])
def test_each_asset_cleans_the_original_geometry_before_shaping(shape):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = CUTOUT_BUILDERS[shape]()
links = [link for link in group.links if link.from_node.bl_idname == "NodeGroupInput" and link.from_socket.name == "Geometry"]
assert len(links) == 1
assert links[0].to_node.bl_idname == "GeometryNodeDeleteGeometry"
def test_full_wrapper_smooth_keeps_closed_shell():
obj, set_value = surface(wrapper=True)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
set_value("Thickness", 0.2)
set_value("Smooth", 2)
coords, faces = evaluated(obj)
@@ -183,7 +173,7 @@ def test_shell_source_indices_cross_identity_grid_rows():
def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
obj, set_value = surface(step=0.0, resolution=resolution)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 1.0)
set_value("Depth Split", 1.0)
coords, faces = evaluated(obj)
if resolution == 1:
assert len(coords) == 0 and not faces
@@ -196,7 +186,7 @@ def test_planar_depth_preserves_blocks_and_cleans_single_face_width(resolution):
def test_small_positive_split_preserves_unsplit_projection():
obj, set_value = surface(step=2.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.01)
set_value("Depth Split", 0.01)
coords, faces = evaluated(obj)
np.testing.assert_allclose(coords, original, atol=1e-6)
assert faces == original_faces
@@ -205,7 +195,7 @@ def test_small_positive_split_preserves_unsplit_projection():
def test_cut_vertices_preserve_camera_rays_and_other_vertices():
obj, set_value = surface(step=4.0)
original, original_faces = evaluated(obj)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
coords, faces = evaluated(obj)
for source_face, result_face in zip(original_faces, faces):
for source_index, result_index in zip(source_face, result_face):
@@ -220,7 +210,7 @@ def test_cut_vertices_preserve_camera_rays_and_other_vertices():
def test_source_surface_sampling_preserves_depth_on_side_walls():
obj, set_value = surface(step=4.0)
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
set_value("Thickness", 0.1)
coords, faces = evaluated(obj)
assert_closed(faces)
@@ -240,7 +230,7 @@ def test_temporary_fields_are_removed_and_user_attributes_survive(thickness, spl
obj.data.attributes.new(".o_anyimage_user_weight", "FLOAT", "POINT")
for value in attribute.data:
value.value = 0.75
set_value("Split Threshold", split)
set_value("Depth Split", split)
set_value("Thickness", thickness)
set_value("Smooth", 2)
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
@@ -279,7 +269,7 @@ def test_zero_center_distance_does_not_split_or_produce_nan():
for li in face.loop_indices:
uv.data[li].uv = (0.25 if face.index == 0 else 0.75, 0.5)
obj.data = mesh
set_value("Split Threshold", 1.0)
set_value("Depth Split", 1.0)
coords, faces = evaluated(obj)
assert len(coords) == 4 and len(faces) == 2
assert np.isfinite(coords).all()
@@ -297,7 +287,7 @@ def test_triangle_corner_weights_do_not_open_sloped_side_walls():
pixels[..., 2] += np.arange(32)[:, None] * 0.02
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.5)
set_value("Depth Split", 0.5)
set_value("Thickness", 0.8)
coords, faces = evaluated(obj)
assert_closed(faces)
@@ -319,34 +309,28 @@ def test_relative_distance_slope_has_density_independent_selection(resolution):
pixels[..., :2] = 0
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Split Threshold", 0.25)
set_value("Depth Split", 0.25)
assert len(evaluated(obj)[0]) == len(obj.data.vertices)
set_value("Split Threshold", 1.0)
set_value("Depth Split", 1.0)
assert len(evaluated(obj)[0]) > len(obj.data.vertices)
@pytest.mark.parametrize("kind", ["plane", "cutout"])
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_split_uses_ray_distance_and_ignores_absolute_distance_scale(kind, source):
def test_split_uses_ray_distance_and_ignores_absolute_distance_scale(kind):
from tests.support.planes import create_surface
if kind == "plane":
obj, set_value, _, _, image = create_surface(source, "DEPTH")
obj, set_value, _, _, image = create_surface("DEPTH")
set_value("Subdivide", 1)
else:
obj, set_value = surface(step=0)
if source == "asset":
from tests.support.paths import PROJECT_ROOT
with bpy.data.libraries.load(str(PROJECT_ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (_, target):
target.node_groups = ["O Image Depth Cutout"]
obj.modifiers[0].node_group = target.node_groups[0]
image = bpy.data.images["Camera"]
output_split_mesh(obj)
set_value("Reference Depth", 3)
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
width, height = image.size
pixels = np.ones((height, width, 4), np.float32)
original_count = len(evaluated(obj)[0])
set_value("Split Threshold", 1)
set_value("Depth Split", 1)
# Equal axial Z, but a tenfold ray-distance jump: Z-only splitting misses it.
pixels[..., :3] = (0, 0, 1)
pixels[:, width // 2:, 0] = np.sqrt(99)

View File

@@ -8,15 +8,11 @@ import pytest
from anyimage.common.object import modifier_input_identifier, set_modifier_input
from tools.nodes.groups.image_depth_panorama import build_image_depth_panorama_group
from tests.support.nodes import validate_node_structure
def panorama(depth=3, alpha=1, subdivide=3):
bpy.ops.wm.read_factory_settings(use_empty=True)
group = build_image_depth_panorama_group()
assert group.name == "O Image Depth Panorama"
validate_node_structure(group)
assert not any(node.bl_idname == "GeometryNodeGroup" for node in group.nodes)
image = bpy.data.images.new("Radial Depth", width=128, height=64, float_buffer=True)
image.colorspace_settings.name = "Non-Color"
image.alpha_mode = "CHANNEL_PACKED"
@@ -37,6 +33,7 @@ def panorama(depth=3, alpha=1, subdivide=3):
for name, value in (("Depth Image", image), ("Subdivide", subdivide)):
set_value(name, value)
set_value("Boundary Smooth", 0)
set_value("Depth Split", 0)
return obj, set_value
@@ -47,7 +44,6 @@ def evaluated(obj):
points = np.array([v.co[:] for v in mesh.vertices]).reshape(-1, 3)
faces = [tuple(f.vertices) for f in mesh.polygons]
uv = np.array([v.uv[:] for v in mesh.uv_layers.active.data]) if mesh.uv_layers.active else np.empty((0, 2))
assert not any(a.name.startswith(("o_depth_", "o_panorama_")) for a in mesh.attributes)
return points, faces, uv
finally:
result.to_mesh_clear()
@@ -75,10 +71,10 @@ def test_quad_sphere_topology_and_radial_endpoints(subdivide):
def test_depth_alpha_culls_faces_without_double_multiplication():
obj, set_value = panorama(alpha=0.5)
assert not len(evaluated(obj)[0])
set_value("Valid Only", False)
set_value("Depth Mask", False)
assert len(evaluated(obj)[1]) == 384
set_value("Valid Only", True)
set_value("Validity Threshold", 0.49)
set_value("Depth Mask", True)
set_value("Mask Threshold", 0.49)
assert len(evaluated(obj)[1]) == 384
@@ -115,7 +111,7 @@ def test_invalid_distance_removes_faces_between_valid_and_invalid_regions(sky_al
assert np.linalg.norm(points, axis=1).max() < 3.01
set_value("Invalid Distance", 250)
assert evaluated(obj)[1] == original_faces
set_value("Valid Only", False)
set_value("Depth Mask", False)
for background in (100, 250):
set_value("Invalid Distance", background)
points, faces, _ = evaluated(obj)
@@ -132,14 +128,13 @@ def test_controls_smoothing_and_material_slot_inheritance():
obj, set_value = panorama()
group = obj.modifiers[0].node_group
inputs = {s.name: s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"}
assert inputs["Validity Threshold"].parent.name == "Options"
assert inputs["Mask Threshold"].parent.name == "Options"
assert inputs["Invalid Distance"].parent.name == "Options"
assert inputs["Invalid Distance"].default_value == 100
assert inputs["Invalid Distance"].subtype == "DISTANCE"
assert inputs["Smooth Weight"].parent.name == "Options"
assert inputs["Valid Only"].parent == inputs["Subdivide"].parent
assert inputs["Valid Only"].default_value
assert "Material" not in inputs
assert inputs["Depth Mask"].parent == inputs["Subdivide"].parent
assert inputs["Depth Mask"].default_value
obj.data.from_pydata([(0, 0, 0)], [], [])
material = bpy.data.materials.new("Slot Material")
obj.data.materials.append(material)
@@ -193,7 +188,7 @@ def test_split_strength_and_distance_scale_invariance():
obj, set_value = panorama(depth * scale)
counts = []
for split in (0, 0.1, 0.5, 1):
set_value("Split Threshold", split)
set_value("Depth Split", split)
points, faces, uv = evaluated(obj)
assert np.isfinite(points).all() and np.isfinite(uv).all()
edges = Counter(tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0])))
@@ -209,7 +204,7 @@ def test_continuous_spherical_slope_does_not_split_at_seam_or_poles():
depth = 3 + 0.3 * np.cos(2 * np.pi * u) * np.sin(np.pi * v)
obj, set_value = panorama(depth)
original = evaluated(obj)
set_value("Split Threshold", 1)
set_value("Depth Split", 1)
set_value("Boundary Smooth", 12)
points, faces, uv = evaluated(obj)
np.testing.assert_allclose(points, original[0], atol=1e-6)
@@ -226,7 +221,7 @@ def test_split_corners_keep_their_rays_and_own_face_distance():
original, _, _ = evaluated(obj)
original_radii = np.linalg.norm(original, axis=1)
original_rays = original / original_radii[:, None]
set_value("Split Threshold", 1)
set_value("Depth Split", 1)
points, _, uv = evaluated(obj)
radii = np.linalg.norm(points, axis=1)
distance, indices = cKDTree(original_rays).query(points / radii[:, None])
@@ -245,7 +240,7 @@ def test_zero_split_bypasses_single_snapshot_strip_cleanup(monkeypatch):
with monkeypatch.context() as patch:
patch.setattr(depth_surface, "_remove_strip_faces", lambda group, geometry, **kw: geometry)
obj, set_value = panorama(depth)
set_value("Split Threshold", 1)
set_value("Depth Split", 1)
_, faces, _ = evaluated(obj)
edges = Counter(tuple(sorted((a, b))) for f in faces for a, b in zip(f, (*f[1:], f[0])))
strips = 0
@@ -255,7 +250,7 @@ def test_zero_split_bypasses_single_snapshot_strip_cleanup(monkeypatch):
assert strips > 0
obj, set_value = panorama(depth)
original_faces = evaluated(obj)[1]
set_value("Split Threshold", 1)
set_value("Depth Split", 1)
assert len(evaluated(obj)[1]) == len(faces) - strips
set_value("Split Threshold", 0)
set_value("Depth Split", 0)
assert evaluated(obj)[1] == original_faces

View File

@@ -50,9 +50,6 @@ def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
def test_depth_layer_keeps_foreground_alpha_threshold(image_layer):
depth = build_image_depth_layer_group()
assert "Alpha Fix" not in {
item.name for item in depth.interface.items_tree if item.item_type == "SOCKET"
}
output = next(node for node in depth.nodes if node.type == "GROUP_OUTPUT")
threshold = output.inputs["Alpha"].links[0].from_node
assert threshold.operation == "GREATER_THAN"

View File

@@ -20,7 +20,7 @@ def evaluated_mesh(obj):
@pytest.mark.parametrize("source_count", [0, 4, 9])
def test_material_inheritance_removes_only_source_vertices(source_count):
from tools.nodes.common.material import inherit_material_slots
from tools.nodes.common.material_slots import inherit_material_slots
from tools.nodes.arrangement.arrange import arrange_nodes
bpy.ops.wm.read_factory_settings(use_empty=True)

View File

@@ -1,27 +1,85 @@
"""Check the saved library's public inventory and persistence."""
import bpy
import pytest
from anyimage.common.node import load_node_group, node_asset_path
from tools.nodes.check import GEOMETRY_NAMES, SHADER_NAMES
from anyimage.common.node import node_asset_path
from tools.nodes.check import GEOMETRY_NAMES
def test_saved_asset_inventory():
def test_saved_assets_evaluate():
from tools.nodes.check import check_asset_library
bpy.ops.wm.read_factory_settings(use_empty=True)
with bpy.data.libraries.load(str(node_asset_path())) as (source, _):
assert set(source.node_groups) == GEOMETRY_NAMES | SHADER_NAMES
for name in sorted(GEOMETRY_NAMES | SHADER_NAMES):
bpy.ops.wm.read_factory_settings(use_empty=True)
node_group = load_node_group(name)
assert node_group.bl_idname == ("GeometryNodeTree" if name in GEOMETRY_NAMES else "ShaderNodeTree")
assert node_group.use_fake_user and node_group.users > 0
if name in GEOMETRY_NAMES:
assert node_group.is_modifier
if bpy.app.version >= (5, 0, 0):
assert node_group.library is not None
if name in {"O Image Depth Layer", "O Shadeless"}:
expected = ["Color", "Alpha", "Depth", "Bump Scale"] if name == "O Image Depth Layer" else ["Color", "Alpha"]
assert [item.name for item in node_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"] == expected
check_asset_library(node_asset_path())
def test_saved_parameter_descriptions_match_definitions():
from tools.nodes.build import build_node_groups
parameters = {
"O Image Plane": {"Subdivide"},
"O Image Depth Plane": {
"Subdivide", "Thickness", "Depth Split", "Smooth", "Smooth Weight",
"Depth Mask", "Mask Threshold", "Boundary Smooth",
},
"O Image Relief Plane": {"Subdivide", "Thickness", "Smooth", "Smooth Weight"},
"O Image Cutout": {"Thickness", "Smooth", "Smooth Weight"},
"O Image Depth Cutout": {
"Thickness", "Depth Split", "Smooth", "Smooth Weight",
"Boundary Smooth", "Outline Depth Fix", "Front Inflation",
},
"O Image Depth Balloon": {"Thickness", "Smooth", "Smooth Weight"},
"O Image Depth Panorama": {
"Subdivide", "Depth Scale", "Depth Split", "Smooth", "Smooth Weight",
"Depth Mask", "Mask Threshold", "Boundary Smooth", "Invalid Distance",
},
"O Image Layer": {"Alpha Fix", "Normal Scale", "Bump Scale", "Object Space"},
"O Image Depth Layer": {"Bump Scale"},
}
def descriptions(groups):
result = {}
for group in groups:
if group.name not in parameters:
continue
sockets = [
item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
and item.name in parameters[group.name]
]
assert {item.name for item in sockets} == parameters[group.name]
assert all(item.description for item in sockets), group.name
if group.name == "O Image Depth Panorama":
defaults = {item.name: item.default_value for item in sockets
if item.name in {"Subdivide", "Depth Split", "Boundary Smooth"}}
assert defaults == pytest.approx({
"Subdivide": 8, "Depth Split": 0.1, "Boundary Smooth": 2,
})
if "Depth Mask" in parameters[group.name]:
names = [item.name for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"]
assert names.index("Depth Mask") == names.index("Depth Split") + 1
if group.name in {"O Image Cutout", "O Image Depth Cutout"}:
mode = next(node for node in group.nodes
if node.bl_idname == "GeometryNodeMenuSwitch"
and node.inputs["Menu"].links[0].from_socket.name == "Mode")
assert [item.name for item in mode.enum_items] == ["Balloon", "Shell"]
thickness = [item.description for item in sockets if item.name == "Thickness"]
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
result[group.name] = [
(item.identifier, item.name, item.description) for item in sockets
]
assert result.keys() == parameters.keys()
return result
bpy.ops.wm.read_factory_settings(use_empty=True)
expected = descriptions(build_node_groups())
bpy.ops.wm.read_factory_settings(use_empty=True)
with bpy.data.libraries.load(str(node_asset_path())) as (library, target):
target.node_groups = list(library.node_groups)
assert descriptions(target.node_groups) == expected
@pytest.mark.parametrize("source", ["definition", "asset"])

View File

@@ -1,37 +0,0 @@
from tests.support.paths import PROJECT_ROOT
import unittest
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
class NodeAssetsTest(unittest.TestCase):
def test_geometry_assets_preserve_node_structure(self):
import bpy
from tests.support.nodes import validate_node_structure
from tools.nodes.arrangement.arrange import arrange_nodes
bpy.ops.wm.read_factory_settings(use_empty=True)
with bpy.data.libraries.load(
str(SOURCE_ROOT / "assets" / "O_AnyImage.blend")
) as (source, target):
target.node_groups = list(source.node_groups)
groups = [
group
for group in target.node_groups
if group.bl_idname == "GeometryNodeTree"
]
self.assertEqual(len(groups), 7)
for group in groups:
validate_node_structure(group)
positions = {node.name: tuple(node.location_absolute) for node in group.nodes
if node.bl_idname not in {"NodeReroute", "NodeGroupInput"}}
arrange_nodes(group)
for name, position in positions.items():
for actual, expected in zip(group.nodes[name].location_absolute, position):
self.assertAlmostEqual(actual, expected, delta=0.01, msg=f"{group.name}: {name}")
if __name__ == "__main__":
unittest.main()

View File

@@ -3,30 +3,22 @@
import bpy
import pytest
from anyimage.common.node import node_asset_path
from tests.support.depth_surface import surface
from tests.support.planes import create_surface
from tests.tools.nodes.test_image_depth_panorama import panorama
@pytest.mark.parametrize("source", ["definition", "asset"])
@pytest.mark.parametrize("kind", ["cutout", "plane", "panorama"])
def test_depth_attribute_cleanup_has_no_warnings(source, kind):
def test_depth_attribute_cleanup_has_no_warnings(kind):
if kind == "cutout":
obj, set_value = surface()
elif kind == "plane":
obj, set_value, *_ = create_surface("definition", "DEPTH")
obj, set_value, *_ = create_surface("DEPTH")
else:
obj, set_value = panorama(subdivide=2)
modifier = obj.modifiers[0]
if source == "asset":
name = modifier.node_group.name
modifier.node_group.name += " Definition"
with bpy.data.libraries.load(str(node_asset_path())) as (_, target):
target.node_groups = [name]
modifier.node_group = target.node_groups[0]
for split in (0.0, 0.5, 0.0):
set_value("Split Threshold", split)
set_value("Depth Split", split)
for smooth in (0, 5):
set_value("Boundary Smooth", smooth)
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())

View File

@@ -1,43 +0,0 @@
from tests.support.paths import PROJECT_ROOT
import tomllib
import unittest
import importlib
from unittest.mock import patch
import pytest
@pytest.mark.parametrize("module_name", ["tools.nodes.cli", "tools.models.cli"])
@pytest.mark.parametrize("arguments,code", [([], None), (["--help"], 0), (["unknown"], 2), (["--bad-option"], 2)])
def test_help_and_invalid_arguments_do_not_start_work(module_name, arguments, code, capsys):
module = importlib.import_module(module_name)
with patch("sys.argv", ["tool", *arguments]), patch.object(module.subprocess, "run") as run:
if code is None:
module.main()
else:
with pytest.raises(SystemExit) as error:
module.main()
assert error.value.code == code
run.assert_not_called()
captured = capsys.readouterr()
assert "usage:" in captured.out + captured.err
class ToolCommandsTests(unittest.TestCase):
def test_repository_tools_have_uv_commands(self):
project = tomllib.loads(
(PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8")
)["project"]
self.assertEqual(
project["scripts"],
{
"pack": "tools.pack:main",
"node-group": "tools.nodes.cli:main",
"model": "tools.models.cli:main",
"docs": "tools.docs:main",
},
)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,42 +0,0 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
SCRIPT_PATH = PROJECT_ROOT / "tools" / "docs.py"
SPEC = importlib.util.spec_from_file_location("docs_script", SCRIPT_PATH)
docs = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(docs)
def test_documentation_command_deploys_project_version(monkeypatch):
monkeypatch.setattr(docs, "project_version", lambda: "1.2.3")
assert docs.documentation_command() == [
"mike",
"deploy",
"--update-aliases",
"1.2.3",
"latest",
]
def test_documentation_command_previews_current_source():
assert docs.documentation_command(dev=True) == ["mkdocs", "serve"]
def test_main_runs_from_project_root(monkeypatch):
calls = []
monkeypatch.setattr(
docs.subprocess,
"run",
lambda *args, **kwargs: calls.append((args, kwargs)),
)
docs.main(["-dev"])
assert calls == [
(
(["mkdocs", "serve"],),
{"cwd": docs.PROJECT_ROOT, "check": True},
)
]

View File

@@ -15,7 +15,6 @@ class ExtensionManifestTest(unittest.TestCase):
self.assertEqual(self.manifest["schema_version"], "1.0.0")
self.assertEqual(self.manifest["id"], "anyimage")
self.assertEqual(self.manifest["type"], "add-on")
self.assertNotIn("bl_info", self.manifest)
def test_declares_required_environment_permissions(self):
self.assertEqual(

View File

@@ -104,19 +104,19 @@ class ExtensionPackagingTest(unittest.TestCase):
"operators/convert_to_panorama/__init__.py",
"operators/convert_to_panorama/operators.py",
"operators/convert_to_panorama/object.py",
"operators/frame_tool.py",
"operators/frame_tool/__init__.py",
"operators/frame_tool/operators.py",
"operators/frame_tool/projection.py",
"operators/frame_tool/compositing.py",
"operators/mask_tool.py",
"operators/rectify_tool.py",
"operators/rectify_tool/__init__.py",
"operators/rectify_tool/operators.py",
"operators/rectify_tool/geometry.py",
"operators/rectify_tool/preview.py",
"operators/ai_setup.py",
"operators/remove_background.py",
):
self.assertIn(path, names)
self.assertNotIn("operators/convert_to_plane/nodes.py", names)
self.assertNotIn("operators/cutout/__init__.py", names)
self.assertNotIn("operators/image_mesh/__init__.py", names)
self.assertNotIn("operators/mesh/__init__.py", names)
self.assertNotIn("operators/image_edit/__init__.py", names)
self.assertNotIn("operators/background.py", names)
self.assertIn(
"assets/O_AnyImage.blend",
names,
@@ -132,26 +132,12 @@ class ExtensionPackagingTest(unittest.TestCase):
self.assertIn("server/geometry/depth_texture.py", names)
self.assertIn("server/geometry/prediction_artifacts.py", names)
self.assertIn("server/geometry/normal_texture.py", names)
self.assertNotIn(
"server/geometry/generate_geometry_and_normal.py", names
)
self.assertNotIn("common/image_selection.py", names)
self.assertNotIn("server/selection/cutout_images.py", names)
self.assertNotIn("server/selection/polygon_mask.py", names)
self.assertNotIn("server/jobs/prepare_geometry_data.py", names)
self.assertNotIn("server/geometry/cutout.py", names)
self.assertIn("server/models/background.py", names)
self.assertNotIn("server/models/da3.py", names)
self.assertIn("server/models/moge.py", names)
self.assertNotIn("server/jobs/debug_predictions.py", names)
self.assertNotIn("operators/debug.py", names)
self.assertFalse(any(name.startswith("server/outputs/") for name in names))
self.assertIn("server/model_catalog.py", names)
self.assertIn("server/model_download.py", names)
self.assertIn("server/models/onnx_ben2.py", names)
self.assertIn("server/models/onnx_birefnet.py", names)
self.assertNotIn("server/models/onnx_pisa_sr.py", names)
self.assertNotIn("server/models/onnx_da3.py", names)
self.assertIn("server/models/onnx_moge2.py", names)
self.assertIn("server/models/onnx_moge3.py", names)
self.assertIn("server/models/onnx_upscale.py", names)
@@ -163,10 +149,7 @@ class ExtensionPackagingTest(unittest.TestCase):
"wheels/blendjob-0.1.17-py3-none-any.whl",
names,
)
self.assertFalse(any(name.startswith("blendjob/") for name in names))
self.assertNotIn("anyimage/__init__.py", names)
self.assertFalse(any(name.startswith("sources/") for name in names))
self.assertFalse(any(name.startswith("vendor/") for name in names))
self.assertIn(f'platforms = ["{platform}"]', manifest)
self.assertNotIn(
"windows-x64" if platform != "windows-x64" else "macos-arm64",