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:
2026-09-09 22:18:59 +08:00
parent 89087fc2e2
commit f48bbb3d05
97 changed files with 436 additions and 705 deletions

View File

@@ -47,7 +47,7 @@ uv run pytest
uv run pack
# 构建并验证 Blender 节点资产
uv run build_node
uv run build_nodes
# 准备、校验并同步 ONNX 模型到 R2
uv run sync_models

View File

@@ -27,7 +27,7 @@ Blender Stub 共用 `blender_test_support.py`;Server 和实际 Blender 几何
| --- | --- |
| `uv run pack` | 生成 Windows、macOS arm64、Linux 扩展包 |
| `uv run pack --prepare` | 为本机同步扩展 wheel 与 Manifest |
| `uv run build_node` | 构建、验证节点资产并运行测试 |
| `uv run build_nodes` | 构建、验证节点资产并运行测试 |
| `uv run sync_models` | 准备、校验并上传模型声明的文件 |
| `uv run docs -dev` | 本地预览文档 |
| `uv run docs` | 通过 Mike 发布项目版本并更新 latest |

View File

@@ -2,7 +2,7 @@
## 构建节点资产
生产节点组由 `uv run build_node` 构建到 `src/anyimage/assets/O_AnyImage.blend`
生产节点组由 `uv run build_nodes` 构建到 `src/anyimage/assets/O_AnyImage.blend`
节点组文件在 `tools/nodes/groups`
@@ -40,8 +40,8 @@
保存后,`check.py` 在独立 Blender 进程中重新加载资产并做基本几何求值。接口、默认值和详细几何行为由 `tests/` 检查。
```bash
uv run build_node --blender "/path/to/blender"
uv run build_node --skip-tests
uv run build_nodes --blender "/path/to/blender"
uv run build_nodes --skip-tests
```
脚本按 `--blender`、环境变量 `BLENDER_BIN`、项目 `.env`、`PATH` 的顺序查找 Blender。无法自动找到时,在项目根目录的 `.env` 中配置:

View File

@@ -27,7 +27,7 @@ dev = [
[project.scripts]
pack = "tools.pack:main"
build_node = "tools.nodes.cli:main"
build_nodes = "tools.nodes.cli:main"
sync_models = "tools.models.cli:main"
docs = "tools.docs:main"
@@ -37,3 +37,7 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["tools"]
[tool.pytest.ini_options]
# Keep native asserts: bpy 4.5 can crash at shutdown after assertion rewriting.
addopts = "--assert=plain"

Binary file not shown.

0
tests/__init__.py Normal file
View File

View File

View File

View File

@@ -1,16 +1,11 @@
from tests.support.paths import PROJECT_ROOT
import importlib
import importlib.util
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
from blender_test_support import mock_bpy_imports
from tests.support.blender import mock_bpy_imports
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src"
sys.path.insert(0, str(SOURCE_ROOT))
def load_common_modules():
fake_bpy = ModuleType("bpy")

View File

@@ -1,5 +1,3 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
@@ -7,7 +5,6 @@ import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common import image as images
from anyimage.operators.convert_to_plane.object import create_color_image

View File

@@ -1,12 +1,7 @@
import json
import sys
from pathlib import Path
from types import SimpleNamespace
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common.selection import SelectionMask
from anyimage.common.depth import (

View File

@@ -1,12 +1,9 @@
import sys
from pathlib import Path
from unittest.mock import patch
import bpy
import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common import image as images

View File

@@ -2,8 +2,7 @@ import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from common_test_support import load_common_modules
PROJECT_ROOT=Path(__file__).resolve().parents[1]
from tests.anyimage.common.support import load_common_modules
class ImageDataTest(unittest.TestCase):
@classmethod

View File

@@ -3,23 +3,11 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class ImageEditToolTest(BlenderTestCase):
def test_image_edit_tools_share_the_plain_pixel_edit_result_creator(self):
self.assertIs(
self.edit_mask.create_image_edit_result,
self.image_data.create_image_edit_result,
)
self.assertIs(
self.rectify.create_image_edit_result,
self.image_data.create_image_edit_result,
)
self.assertFalse(hasattr(self.image_data, "create_pixel_result_image"))
def test_packed_image_export_does_not_use_scene_render_transform(self):
@@ -222,12 +210,3 @@ class ImageEditToolTest(BlenderTestCase):
"Select an active Image Empty",
):
self.image_interaction.active_image_edit_source(context)
def test_blender_image_exports_do_not_use_scene_render_color_management(self):
source_root = PROJECT_ROOT / "src" / "anyimage"
offenders = []
for path in source_root.rglob("*.py"):
if ".save_render(" in path.read_text(encoding="utf-8"):
offenders.append(path.relative_to(source_root))
self.assertEqual(offenders, [])

View File

@@ -1,21 +1,14 @@
import sys
from pathlib import Path
from types import SimpleNamespace
import bpy
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import image as image_data
from anyimage.common import material as material_data
from anyimage.operators.convert_to_plane import object as plane_object
from anyimage.operators.cutout_tool import object as cutout_object
from anyimage.operators.cutout_tool import shape as cutout_shape
from image_object_test_support import _create_cutout_shape, _group_node, _inputs
from tests.support.image_objects import _create_cutout_shape, _group_node, _inputs
def test_image_layer_switches_between_tangent_and_object_space_normal():

View File

@@ -0,0 +1,27 @@
import bpy
import pytest
from types import SimpleNamespace
from anyimage.common import material
from tests.support.materials import image_layer
@pytest.mark.parametrize("enabled", [False, True])
@pytest.mark.parametrize("shadeless", [False, True])
@pytest.mark.parametrize("view", ["AgX", "Unknown"])
def test_material_alpha_strength_follows_preference(image_layer, monkeypatch, enabled, shadeless, view):
monkeypatch.setattr(material, "material_node_group", lambda depth_plane=False: image_layer)
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: enabled)
color = bpy.data.images.new("Alpha edge", width=2, height=2, alpha=True)
scene = SimpleNamespace(view_settings=SimpleNamespace(view_transform=view))
result = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
layer = next(node for node in result.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
assert layer.inputs["Alpha Fix"].default_value == float(enabled)
alpha_link = layer.outputs["Alpha"].links[0]
assert alpha_link.to_node.type == ("MIX_SHADER" if shadeless else "BSDF_PRINCIPLED")
assert alpha_link.to_socket.name == ("Fac" if shadeless else "Alpha")
layer.inputs["Alpha Fix"].default_value = 0.5
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: not enabled)
second = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
second_layer = next(node for node in second.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
assert second_layer.inputs["Alpha Fix"].default_value == float(not enabled)
assert layer.inputs["Alpha Fix"].default_value == 0.5

View File

@@ -1,8 +1,6 @@
import unittest
from pathlib import Path
from types import SimpleNamespace
from common_test_support import load_common_modules
PROJECT_ROOT=Path(__file__).resolve().parents[1]
from tests.anyimage.common.support import load_common_modules
class MaterialSettingsTest(unittest.TestCase):
@classmethod

View File

@@ -1,5 +1,3 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
@@ -7,7 +5,6 @@ import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage import preferences
from anyimage.common import image as images

View File

@@ -1,8 +1,6 @@
import unittest
from pathlib import Path
from types import SimpleNamespace
from common_test_support import load_common_modules
PROJECT_ROOT=Path(__file__).resolve().parents[1]
from tests.anyimage.common.support import load_common_modules
class ModifierInputsTest(unittest.TestCase):
@classmethod

View File

@@ -1,7 +1,6 @@
from tests.support.paths import PROJECT_ROOT
import unittest
from pathlib import Path
from common_test_support import load_common_modules
PROJECT_ROOT=Path(__file__).resolve().parents[1]
from tests.anyimage.common.support import load_common_modules
class NodeLoadingTest(unittest.TestCase):
@classmethod

View File

@@ -1,11 +1,7 @@
import json
import sys
from pathlib import Path
import numpy as np
import pytest
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import selection

View File

View File

@@ -1,7 +1,7 @@
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class AiImageOperatorsTest(BlenderTestCase):

View File

@@ -3,7 +3,7 @@ import tempfile
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class AiSetupTest(BlenderTestCase):

View File

@@ -1,14 +1,6 @@
import sys
from pathlib import Path
from unittest.mock import patch
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.operators.cutout_tool import geometry as cutout_geometry
from anyimage.operators.cutout_tool.geometry import poisson_balloon_profile

View File

@@ -1,11 +1,9 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common import selection, viewport
from anyimage.operators import mask_tool as mask

View File

@@ -1,16 +1,12 @@
from tests.support.paths import PROJECT_ROOT
import importlib
import struct
import sys
import tempfile
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import mock_bpy_imports
PROJECT_ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
from tests.support.blender import mock_bpy_imports
class ClipboardImageTest(unittest.TestCase):

View File

@@ -0,0 +1,34 @@
import bpy
import numpy as np
import pytest
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.operators.cutout_tool.operators import normal_mode_for_shape
from anyimage.operators.cutout_tool.geometry import refine_balloon_triangles, poisson_balloon_profile
@pytest.mark.parametrize("enabled", [False, True])
def test_depth_presets_always_generate_the_correct_normal_space(enabled):
assert normal_mode_for_shape("DEPTH_SOLID", enabled) == "OBJECT"
assert normal_mode_for_shape("DEPTH_BALLOON", enabled) == "TANGENT"
for shape in ("FLAT", "SOLID"):
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
def test_new_cutout_does_not_reuse_a_saved_wrapper():
bpy.ops.wm.read_factory_settings(use_empty=True)
saved = bpy.data.node_groups.new("O Image Cutout", "GeometryNodeTree")
saved.interface.new_socket(name="Shape", in_out="INPUT", socket_type="NodeSocketMenu")
saved.use_fake_user = True
current = load_cutout_node_group("SOLID")
assert current != saved and current.get("anyimage_asset_revision") == 11
assert saved in bpy.data.node_groups.values()
assert current == load_cutout_node_group("FLAT")
assert any(item.name == "Shape" for item in saved.interface.items_tree)
def test_refined_boundary_ears_have_interior_balloon_samples():
points = np.array([(0, 0), (1, 0), (1, 1), (0, 1)], dtype=float)
points, faces, boundary = refine_balloon_triangles(points, [(0, 1, 2), (0, 2, 3)], np.ones(4, bool))
profile = poisson_balloon_profile(points, faces, boundary)
assert np.all(profile[boundary] == 0)
assert all(any(profile[index] > 0 for index in face) for face in faces)

View File

@@ -1,12 +1,9 @@
import sys
from pathlib import Path
import numpy as np
import pytest
from scipy.sparse import coo_matrix
from scipy.sparse.csgraph import connected_components
from scipy.spatial import cKDTree
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.operators.cutout_tool import geometry as g
from anyimage.operators.cutout_tool import fine_mesh as fine

View File

@@ -1,15 +1,9 @@
import builtins
import unittest
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common.selection import SelectionPath, rasterize_selection_path
from anyimage.operators.cutout_tool import geometry as cutout_geometry

View File

@@ -1,10 +1,6 @@
import json
import sys
from pathlib import Path
from types import SimpleNamespace
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common.selection import SelectionPath
from anyimage.common import viewport

View File

@@ -1,5 +1,3 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
@@ -7,9 +5,6 @@ import numpy as np
import pytest
from mathutils import Matrix, Vector
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import depth as depth_data
from anyimage.common import image as image_data
@@ -24,7 +19,7 @@ from anyimage.server.geometry.depth_texture import (
write_depth_texture,
)
from anyimage.server.models.geometry import GeometryFrame
from image_object_test_support import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node, _inputs
from tests.support.image_objects import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node, _inputs
def test_object_normal_image_follows_cutout_inward_axis():

View File

@@ -3,7 +3,7 @@ import json
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class CutoutOperatorsTest(BlenderTestCase):

View File

@@ -1,10 +1,7 @@
import sys
from pathlib import Path
from unittest.mock import patch
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common.image import premultiplied_rgba
from anyimage.operators import frame_tool as frame

View File

@@ -1,9 +1,6 @@
import sys
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common.selection import clip_polygon_halfplanes
from anyimage.operators import frame_tool as frame

View File

@@ -1,6 +1,6 @@
from types import SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class FrameToolTest(BlenderTestCase):

View File

@@ -1,14 +1,8 @@
import sys
from pathlib import Path
from types import SimpleNamespace
import bpy
import numpy as np
from mathutils import Matrix
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import image as image_data
from anyimage.common.selection import SelectionMask

View File

@@ -1,7 +1,7 @@
import sys
from types import ModuleType, SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class MaskToolTest(BlenderTestCase):

View File

@@ -1,11 +1,8 @@
import sys
from pathlib import Path
from unittest.mock import patch
import bpy
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common import image, viewport
from anyimage.operators import mask_tool as mask

View File

@@ -0,0 +1,69 @@
import bpy
import numpy as np
import pytest
from tests.support.planes import surface
def test_conversion_preserves_offset_world_placement_and_initializes_inputs(surface, monkeypatch):
from anyimage.operators.convert_to_plane import object as conversion
from anyimage.common.image import image_empty_bounds
from mathutils import Matrix, Vector
obj, _, _, inputs, image = surface
group = obj.modifiers[0].node_group
monkeypatch.setattr(conversion, "load_node_group", lambda name: group)
source = bpy.data.objects.new("Offset Image", None)
source.empty_display_type = "IMAGE"
source.data = image
source.empty_display_size = 4
source.empty_image_offset = (-0.2, -0.8)
source.matrix_world = Matrix.Translation((3, -2, 5)) @ Matrix.Rotation(0.7, 4, "Y") @ Matrix.Diagonal((2, 3, 1, 1))
bpy.context.collection.objects.link(source)
bounds = image_empty_bounds(source)
matrix = source.matrix_world.copy()
material = bpy.data.materials.new("Conversion")
metadata = {"image_size": (16, 8), "reference_depth": 2.0,
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
result = conversion.create_depth_plane_object(
bpy.context, source, 2, material, image, metadata, "DEPTH")
assert result.name == "Offset Image"
assert result["anyimage_mesh_shape"] == "DEPTH_PLANE"
assert bpy.context.view_layer.objects.active == result
expected = sorted(tuple(matrix @ Vector((x, y, 0)))
for x in bounds[:2] for y in bounds[2:])
actual = sorted(tuple(result.matrix_world @ v.co) for v in result.data.vertices)
np.testing.assert_allclose(actual, expected, atol=1e-6)
center = matrix @ Vector(((bounds[0] + bounds[1]) / 2, (bounds[2] + bounds[3]) / 2, 0))
np.testing.assert_allclose(result.matrix_world.translation, center, atol=1e-6)
modifier = result.modifiers[0]
assert modifier[inputs["Thickness"].identifier] == 0
assert modifier[inputs["Depth Image"].identifier] == image
assert modifier[inputs["Valid Only"].identifier] is True
assert modifier[inputs["Reference Depth"].identifier] == pytest.approx(
2 * modifier[inputs["Uniform Scale"].identifier])
assert result.data.materials[0] == material
def test_failed_modifier_initialization_removes_partial_object(surface, monkeypatch):
from anyimage.operators.convert_to_plane import object as conversion
obj, _, _, _, image = surface
monkeypatch.setattr(conversion, "load_node_group", lambda name: obj.modifiers[0].node_group)
source = bpy.data.objects.new("Source", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
material = bpy.data.materials.new("Unused")
before_objects, before_meshes = set(bpy.data.objects), set(bpy.data.meshes)
def fail(*args):
raise RuntimeError("input failed")
monkeypatch.setattr(conversion, "set_modifier_input", fail)
metadata = {"image_size": (16, 8), "reference_depth": 2,
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
with pytest.raises(RuntimeError, match="input failed"):
conversion.create_depth_plane_object(bpy.context, source, 2, material, image, metadata, "DEPTH")
assert set(bpy.data.objects) == before_objects
assert set(bpy.data.meshes) == before_meshes
assert material.users == 0

View File

@@ -1,21 +1,15 @@
import sys
from pathlib import Path
from unittest.mock import patch
import bpy
import numpy as np
from mathutils import Matrix, Vector
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import image as image_data
from anyimage.common import object as object_data
from anyimage.operators.clipboard_image import actions as clipboard_actions
from anyimage.operators.convert_to_plane import object as plane_object
from anyimage.operators.cutout_tool import shape as cutout_shape
from image_object_test_support import _depth_image, _depth_metadata, _evaluated_positions, _inputs
from tests.support.image_objects import _depth_image, _depth_metadata, _evaluated_positions, _inputs
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

View File

@@ -1,7 +1,7 @@
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class PlaneOperatorsTest(BlenderTestCase):

View File

@@ -1,5 +1,5 @@
from types import SimpleNamespace
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class RectifyToolTest(BlenderTestCase):

View File

@@ -1,12 +1,9 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import bpy
import numpy as np
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.operators.upscale import UpscaleImage
from anyimage.common.image import image_pixels

View File

View File

@@ -1,3 +1,4 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import sys
import tempfile
@@ -6,8 +7,6 @@ import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_server_module():
server_directory = PROJECT_ROOT / "src" / "anyimage" / "server"
specification = importlib.util.spec_from_file_location(

View File

@@ -1,11 +1,9 @@
from tests.support.paths import PROJECT_ROOT
import sys
from pathlib import Path
from types import SimpleNamespace
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_server_module(name):
addon_directory = PROJECT_ROOT / "src" / "anyimage"
sys.path.insert(0, str(addon_directory))

View File

@@ -1,12 +1,9 @@
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).parents[1] / "src" / "anyimage"))
from server.geometry import prediction_artifacts as artifacts
from server.geometry.edge_depth import extend_edge_depth

View File

@@ -3,7 +3,7 @@ import json
import sys
from types import SimpleNamespace
from unittest.mock import patch
from server_runtime_test_support import ServerTestCase
from tests.anyimage.server.runtime_support import ServerTestCase
class GeometryArtifactsTest(ServerTestCase):
def test_depth_alpha_multiplies_coverage_without_changing_geometry(self):

View File

@@ -1,11 +1,8 @@
import threading
import time
from types import SimpleNamespace
from unittest.mock import patch
from server_runtime_test_support import ServerTestCase
from tests.anyimage.server.runtime_support import ServerTestCase
class JobServerTest(ServerTestCase):
def test_dedicated_server_runs_only_one_job_at_a_time(self):
@@ -171,4 +168,3 @@ class JobServerTest(ServerTestCase):
"generate-cutout-artifacts",
},
)

View File

@@ -1,7 +1,7 @@
import tempfile
import unittest
from pathlib import Path
from server_test_support import load_server_module, _make_fake_av
from tests.anyimage.server.support import load_server_module, _make_fake_av
class MediaInputTest(unittest.TestCase):

View File

@@ -1,22 +1,11 @@
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
from PIL import Image
SOURCE_DIR = (
Path(__file__).resolve().parents[1]
/ "src"
/ "anyimage"
/ "server"
/ "models"
)
sys.path.insert(0, str(SOURCE_DIR))
import onnx_ben2
from server.models import onnx_ben2
class FakeSession:

View File

@@ -1,9 +1,6 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_module(name, relative_path):

View File

@@ -1,3 +1,4 @@
from tests.support.paths import PROJECT_ROOT
import os
import sys
import unittest
@@ -7,9 +8,6 @@ from types import SimpleNamespace
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_download_module():
addon_directory = PROJECT_ROOT / "src" / "anyimage"
sys.path.insert(0, str(addon_directory))

View File

@@ -5,7 +5,7 @@ import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
from server_test_support import load_server_module
from tests.anyimage.server.support import load_server_module
class ModelDownloadJobsTest(unittest.TestCase):

View File

@@ -1,14 +1,6 @@
import sys
from pathlib import Path
from unittest.mock import patch
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.server.models import moge2 as moge2_model
from anyimage.server.models.geometry import GeometryFrame

View File

@@ -6,7 +6,7 @@ from unittest.mock import patch
import numpy as np
import pytest
from PIL import Image
from server_test_support import load_server_module, FakeModels
from tests.anyimage.server.support import load_server_module, FakeModels
class ModelInferenceTest(unittest.TestCase):

View File

@@ -1,11 +1,9 @@
import sys
import tempfile
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
from server_runtime_test_support import ServerTestCase
from tests.anyimage.server.runtime_support import ServerTestCase
class ModelManagerTest(ServerTestCase):
def test_required_model_download_skips_ready_model(self):
@@ -182,5 +180,3 @@ class ModelManagerTest(ServerTestCase):
lambda: cache.get_upscale("upscale", "cuda"),
"release_upscale",
)

View File

@@ -1,3 +1,4 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import sys
import unittest
@@ -7,9 +8,6 @@ from unittest.mock import patch
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_module():
anyimage = ModuleType("anyimage")
anyimage.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]

View File

@@ -1,20 +1,10 @@
import sys
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
SOURCE_DIR = (
Path(__file__).resolve().parents[1]
/ "src"
/ "anyimage"
/ "server"
/ "models"
)
sys.path.insert(0, str(SOURCE_DIR))
import onnx_runtime
from server.models import onnx_runtime
class FakeSession:
@@ -211,6 +201,5 @@ class OnnxRuntimeTest(unittest.TestCase):
self.assertEqual(providers, ["CPUExecutionProvider"])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,13 +1,10 @@
import hashlib
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
import pytest
from PIL import Image
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src/anyimage"))
from server.models import onnx_pisa_sr as pisa
from server.model_manager import ModelManager, ModelSessionCache
from server import model_catalog

View File

@@ -1,22 +1,11 @@
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import numpy as np
from PIL import Image
SOURCE_DIR = (
Path(__file__).resolve().parents[1]
/ "src"
/ "anyimage"
/ "server"
/ "models"
)
sys.path.insert(0, str(SOURCE_DIR))
import onnx_upscale
from server.models import onnx_upscale
class FakeSession:

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
import numpy as np
from server_test_support import load_server_module, FakeJobContext
from tests.anyimage.server.support import load_server_module, FakeJobContext
class ServerJobsTest(unittest.TestCase):

View File

@@ -2,7 +2,7 @@ import importlib
import sys
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class AddonRegistrationTest(BlenderTestCase):

View File

@@ -1,6 +1,6 @@
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class ImageMenuTest(BlenderTestCase):
@@ -94,8 +94,6 @@ class ImageMenuTest(BlenderTestCase):
self.assertNotIn(("anyimage.setup_ai_environment", True), calls)
def test_image_operators_stay_clickable_until_server_is_busy(self):
context = SimpleNamespace(
object=SimpleNamespace(data=SimpleNamespace()),

View File

@@ -6,7 +6,7 @@ import threading
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class JobOperatorTest(BlenderTestCase):

View File

@@ -2,7 +2,7 @@ import importlib
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class PreferencesTest(BlenderTestCase):

View File

@@ -1,7 +1,7 @@
import importlib
from types import SimpleNamespace
from unittest.mock import patch
from blender_test_support import BlenderTestCase
from tests.support.blender import BlenderTestCase
class ServerPanelTest(BlenderTestCase):

View File

@@ -1,5 +1,5 @@
"""Expose project source packages to the test suite."""
"""Expose the extension and standalone server packages to the test suite."""
import sys
from pathlib import Path
from tests.support.paths import PROJECT_ROOT
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
sys.path[:0] = [str(PROJECT_ROOT / "src"), str(PROJECT_ROOT / "src/anyimage")]

View File

View File

@@ -1,12 +1,11 @@
from tests.support.paths import PROJECT_ROOT
import importlib
import builtins
import sys
import unittest
from pathlib import Path
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src"

View File

@@ -1,18 +1,11 @@
"""Behavior checks for face separation and connected Depth Surface shells."""
import sys
from pathlib import Path
from collections import Counter
import bpy
import numpy as np
ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(ROOT / "src"))
from node_asset_test_support import CUTOUT_BUILDERS
from tests.support.nodes import CUTOUT_BUILDERS
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):

View File

@@ -1,14 +1,7 @@
import sys
from pathlib import Path
import bpy
import numpy as np
SOURCE_ROOT = Path(__file__).parents[1] / "src"
sys.path.insert(0, str(SOURCE_ROOT))
from anyimage.common import image as image_data
from anyimage.common import object as object_data
from anyimage.common.selection import SelectionMask

View 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)

4
tests/support/paths.py Normal file
View 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
View 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()

View File

@@ -1,145 +0,0 @@
import bpy
from node_asset_test_support import validate_node_structure
from anyimage.common.material import (
DEPTH_MATERIAL_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
)
from anyimage.common.node import load_node_group, node_asset_path
from anyimage.operators.cutout_tool.shape import (
CUTOUT_NODE_GROUP_NAMES,
)
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
RELIEF_PLANE_NODE_GROUP_NAME = "O Image Relief Plane"
def input_names(group):
return [
item.name
for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
]
def test_saved_asset_contracts():
bpy.ops.wm.read_factory_settings(use_empty=True)
image_plane = load_node_group("O Image Plane")
depth_plane = load_node_group(DEPTH_PLANE_NODE_GROUP_NAME)
relief_plane = load_node_group(RELIEF_PLANE_NODE_GROUP_NAME)
image_layer = load_node_group(IMAGE_MATERIAL_NODE_GROUP_NAME)
depth_layer = load_node_group(DEPTH_MATERIAL_NODE_GROUP_NAME)
shadeless = load_node_group("O Shadeless")
cutout_groups = {
name: load_node_group(name) for name in set(CUTOUT_NODE_GROUP_NAMES.values())
}
expected_names = {
"O Image Plane",
DEPTH_PLANE_NODE_GROUP_NAME,
RELIEF_PLANE_NODE_GROUP_NAME,
IMAGE_MATERIAL_NODE_GROUP_NAME,
DEPTH_MATERIAL_NODE_GROUP_NAME,
"O Shadeless",
*CUTOUT_NODE_GROUP_NAMES.values(),
}
with bpy.data.libraries.load(str(node_asset_path()), link=False) as (source, _):
assert set(source.node_groups) == expected_names
assert image_plane.name == "O Image Plane"
assert image_plane.bl_idname == "GeometryNodeTree"
assert image_plane.is_modifier
assert input_names(image_plane) == [
"Geometry",
"Subdivide",
"Thickness",
]
for group, normal_controls in ((depth_plane, ["Normal Smooth", "Smooth Weight", "Validity Threshold"]), (relief_plane, [])):
assert group.bl_idname == "GeometryNodeTree" and group.is_modifier
assert input_names(group) == [
"Geometry", "Subdivide", "Thickness", "Depth Scale",
*(["Smooth", "Valid Only"] if group == depth_plane else []), "Reference Depth",
*normal_controls, "Uniform Scale", "Depth Image",
]
inputs = {item.name: item for item in group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
assert inputs["Thickness"].default_value == 0
assert inputs["Thickness"].subtype == inputs["Reference Depth"].subtype == "DISTANCE"
assert inputs["Subdivide"].default_value == 6
assert all(item.structure_type == "SINGLE" for item in inputs.values())
panels = [item for item in group.interface.items_tree if item.item_type == "PANEL"]
assert [panel.name for panel in panels] == ["Options", "Data"]
assert [item.name for item in panels[0].interface_items] == ["Reference Depth", *normal_controls]
assert [item.name for item in panels[1].interface_items] == ["Uniform Scale", "Depth Image"]
assert all(panel.default_closed for panel in panels)
assert all(item.hide_in_modifier for item in panels[1].interface_items)
assert image_layer.bl_idname == "ShaderNodeTree"
assert input_names(image_layer) == [
"Color",
"Alpha",
"Normal",
"Bump Scale",
"Alpha Fix",
"Normal Scale",
"Object Space",
]
assert depth_layer.bl_idname == "ShaderNodeTree"
adjustment = next(
item for item in image_layer.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"
and item.name == "Alpha Fix"
)
assert adjustment.default_value == 0.0
assert (adjustment.min_value, adjustment.max_value, adjustment.subtype) == (0, 1, "FACTOR")
image_output = next(node for node in image_layer.nodes if node.type == "GROUP_OUTPUT")
alpha_curve = image_output.inputs["Alpha"].links[0].from_node
assert alpha_curve.bl_idname == "ShaderNodeFloatCurve"
assert alpha_curve.inputs["Value"].links[0].from_socket.name == "Alpha"
assert alpha_curve.inputs["Factor"].links[0].from_socket.name == "Alpha Fix"
panels = [item for item in image_layer.interface.items_tree if item.item_type == "PANEL"]
assert len(panels) == 1 and panels[0].name == "Options" and panels[0].default_closed
assert [item.name for item in panels[0].interface_items] == ["Alpha Fix", "Normal Scale", "Object Space"]
alpha_points = alpha_curve.mapping.curves[0].points
assert len(alpha_points) == 4
for point, expected in zip(alpha_points, ((0, 0), (0.8, 0), (0.925, 0.925), (1, 1))):
assert all(abs(actual - value) < 1e-6 for actual, value in zip(point.location, expected))
assert input_names(depth_layer) == [
"Color",
"Alpha",
"Depth",
"Bump Scale",
]
assert shadeless.name == "O Shadeless"
assert shadeless.bl_idname == "ShaderNodeTree"
assert input_names(shadeless) == ["Color"]
assert set(cutout_groups) == set(CUTOUT_NODE_GROUP_NAMES.values())
for group in cutout_groups.values():
assert group.bl_idname == "GeometryNodeTree"
assert group.is_modifier
assert input_names(group)[0] == "Geometry"
public_groups = {
image_plane,
depth_plane,
relief_plane,
image_layer,
depth_layer,
shadeless,
*cutout_groups.values(),
}
loaded = {*public_groups, *cutout_groups.values()}
for group in loaded:
if group.bl_idname == "GeometryNodeTree":
validate_node_structure(group)
assert all(group.use_fake_user for group in public_groups)
assert all(group.users > 0 for group in cutout_groups.values())
if bpy.app.version >= (5, 0, 0):
assert all(group.library is not None for group in loaded)
print(
f"Validated Blender {bpy.app.version_string}: "
+ ", ".join(sorted(group.name for group in loaded))
)

0
tests/tools/__init__.py Normal file
View File

View File

View File

@@ -1,13 +1,10 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_sync_models():
path = PROJECT_ROOT / "tools" / "models" / "cli.py"
spec = importlib.util.spec_from_file_location("sync_models", path)

View File

View File

@@ -8,7 +8,7 @@ import pytest
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
from node_asset_test_support import CUTOUT_BUILDERS
from tests.support.nodes import CUTOUT_BUILDERS
from tools.nodes.arrangement.arrange import BRANCH_GAP, FIELD_HEIGHT, ROUTE_RUN, arrange_nodes
from tools.nodes.arrangement.flow import geometry_paths, origin_socket, peer_links
from tools.nodes.arrangement.dimensions import RULES, estimate_node_dimensions

View File

@@ -1,10 +1,9 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
from pathlib import Path
from unittest.mock import patch
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = PROJECT_ROOT / "tools" / "nodes" / "cli.py"
@@ -78,7 +77,7 @@ def test_launcher_runs_asset_build_and_validations_before_tests(monkeypatch, tmp
calls = []
monkeypatch.setattr(launcher, "blender_path", lambda _: executable)
monkeypatch.setattr(launcher, "run", calls.append)
monkeypatch.setattr("sys.argv", ["build_node"])
monkeypatch.setattr("sys.argv", ["build_nodes"])
launcher.main()

View File

@@ -3,17 +3,20 @@ from collections import Counter
import bpy
import numpy as np
import pytest
from tests.support.nodes import CUTOUT_BUILDERS
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.common.object import modifier_input_identifier, set_modifier_input
from node_asset_test_support import validate_node_structure
from tests.support.nodes import validate_node_structure
def test_saved_cutout_defaults():
@pytest.mark.parametrize("source", ["definition", "asset"])
def test_cutout_interfaces_and_thickness_modes(source):
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {shape: load_cutout_node_group(shape) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
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()):
@@ -80,4 +83,3 @@ def test_saved_cutout_defaults():
assert set(uses.values()) == {2}
finally:
evaluated.to_mesh_clear()
print("Validated three Cutout assets, four presets, both thickness modes and closed defaults")

View File

@@ -1,70 +1,17 @@
"""Contracts for smooth depth volume, editable presets and normal masks."""
from pathlib import Path
import sys
import bpy
import numpy as np
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from node_asset_test_support import CUTOUT_BUILDERS
from tools.nodes.groups.image_depth_cutout import build_volume_fields
from tools.nodes.groups.image_depth_cutout import build_volume_rounding
from anyimage.operators.cutout_tool.object import load_cutout_node_group
from anyimage.operators.cutout_tool.operators import normal_mode_for_shape
from anyimage.operators.cutout_tool.geometry import refine_balloon_triangles, poisson_balloon_profile
from anyimage.common.object import modifier_input_identifier, set_modifier_input
@pytest.mark.parametrize("enabled", [False, True])
def test_depth_presets_always_generate_the_correct_normal_space(enabled):
assert normal_mode_for_shape("DEPTH_SOLID", enabled) == "OBJECT"
assert normal_mode_for_shape("DEPTH_BALLOON", enabled) == "TANGENT"
for shape in ("FLAT", "SOLID"):
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
def test_new_cutout_does_not_reuse_a_saved_wrapper():
bpy.ops.wm.read_factory_settings(use_empty=True)
saved = bpy.data.node_groups.new("O Image Cutout", "GeometryNodeTree")
saved.interface.new_socket(name="Shape", in_out="INPUT", socket_type="NodeSocketMenu")
saved.use_fake_user = True
current = load_cutout_node_group("SOLID")
assert current != saved and current.get("anyimage_asset_revision") == 11
assert saved in bpy.data.node_groups.values()
assert current == load_cutout_node_group("FLAT")
assert any(item.name == "Shape" for item in saved.interface.items_tree)
def test_refined_boundary_ears_have_interior_balloon_samples():
points = np.array([(0, 0), (1, 0), (1, 1), (0, 1)], dtype=float)
points, faces, boundary = refine_balloon_triangles(points, [(0, 1, 2), (0, 2, 3)], np.ones(4, bool))
profile = poisson_balloon_profile(points, faces, boundary)
assert np.all(profile[boundary] == 0)
assert all(any(profile[index] > 0 for index in face) for face in faces)
def test_depth_rounding_options_are_consolidated():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = CUTOUT_BUILDERS["DEPTH_SOLID"]()
inputs = {s.name: s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"}
for name, default in (("Edge Round", 0.2), ("Front Inflation", 1), ("Normal Smooth", 50)):
assert inputs[name].default_value == pytest.approx(default)
assert inputs[name].structure_type == "SINGLE"
assert inputs["Edge Round"].parent.name == "Options"
names = list(inputs)
assert names[names.index("Front Inflation") - 1] == "Edge Round"
assert [s.name for s in inputs.values() if s.parent and s.parent.name == "Options"] == [
"Cleanup Threshold", "Split Threshold", "Reference Depth", "Edge Round", "Front Inflation", "Normal Smooth", "Smooth Weight",
]
assert not set(inputs) & {"Edge Round Range", "Back Rim Range", "Normal Influence", "Edge Round Amount", "Back Rim Fullness"}
assert all(n.inputs["Name"].default_value != "unused" for n in group.nodes if n.bl_idname == "GeometryNodeRemoveAttribute")
@pytest.mark.parametrize("iterations", [0, 50])
def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
from scipy.spatial import cKDTree
from depth_surface_test_support import surface, evaluated, assert_closed
from tests.support.depth_surface import surface, evaluated, assert_closed
obj, set_value = surface(step=0, resolution=4)
image = bpy.data.images["Camera"]
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
@@ -83,7 +30,7 @@ def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
def test_balloon_shell_converges_to_front_with_thickness():
from scipy.spatial import cKDTree
from depth_surface_test_support import surface, evaluated, assert_closed
from tests.support.depth_surface import surface, evaluated, assert_closed
obj, set_value = surface(step=1, 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])
@@ -196,7 +143,7 @@ def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
def test_depth_cutout_normal_map_only_affects_front_faces():
from depth_surface_test_support import surface
from tests.support.depth_surface import surface
obj, set_value = surface()
set_value("Thickness", 0.3)
bpy.context.view_layer.update()
@@ -215,7 +162,7 @@ def test_depth_cutout_normal_map_only_affects_front_faces():
@pytest.mark.parametrize("structure", [0, 1, 4])
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
def test_uniform_front_inflation_scales_with_distance(thickness, structure):
from depth_surface_test_support import surface, evaluated, assert_closed
from tests.support.depth_surface import surface, evaluated, assert_closed
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])
@@ -243,7 +190,7 @@ def test_uniform_front_inflation_scales_with_distance(thickness, structure):
def test_normal_smooth_controls_front_and_rear_directions():
from depth_surface_test_support import surface, evaluated
from tests.support.depth_surface import surface, evaluated
obj, set_value = surface(step=1, resolution=10)
set_value("Front Inflation", 1.0)
xy = np.array([v.co[:2] for v in obj.data.vertices])
@@ -272,7 +219,7 @@ def test_normal_smooth_controls_front_and_rear_directions():
def test_balloon_suppression_is_stronger_without_a_startup_threshold():
from depth_surface_test_support import surface, evaluated
from tests.support.depth_surface import surface, evaluated
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])

View File

@@ -1,90 +1,15 @@
"""Evaluate complete-image projection and rectangular surface topology."""
from collections import Counter
from pathlib import Path
import bpy
import numpy as np
import pytest
ROOT = Path(__file__).parents[1]
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
from node_asset_test_support import validate_node_structure
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.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()
@pytest.mark.parametrize("subdivide", [0, 1, 3])
def test_zero_depth_matches_plane_geometry_and_uv(surface, subdivide):
obj, set_value, plane, inputs, _ = surface
@@ -148,6 +73,41 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness):
assert not any(name.startswith("o_depth_") for name in attributes)
@pytest.mark.parametrize("valid_only", [False, True])
def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
from tests.support.depth_surface import component_count
obj, set_value, _, _, image = surface
pixels = np.asarray(image.pixels[:], dtype=np.float32).reshape(8, 16, 4)
pixels[:, 8:, 2] += 10
pixels[:2, :, 3] = 0
image.pixels.foreach_set(pixels.ravel())
image.update()
set_value("Depth Scale", 1.0)
set_value("Valid Only", valid_only)
base = evaluated(obj)
set_value("Split Threshold", 1.0)
points, faces, uv, materials, indices, attributes = 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)
edges = Counter(tuple(sorted((a, b))) for face in shell_faces
for a, b in zip(face, (*face[1:], face[0])))
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)
restored = evaluated(obj)
np.testing.assert_allclose(restored[0], base[0])
assert restored[1] == base[1]
def test_depth_jump_keeps_rectangular_faces(surface):
obj, set_value, _, _, image = surface
base = evaluated(obj)
@@ -229,7 +189,12 @@ def test_curved_shell_smooths_normals_without_shrinking_thickness(surface, norma
def test_surface_interface_and_node_structure(surface):
obj, _, _, inputs, _ = surface
assert list(inputs) == ["Geometry", "Subdivide", "Thickness", "Depth Scale", "Smooth", "Valid Only",
"Reference Depth", "Normal Smooth", "Smooth Weight", "Validity Threshold", "Uniform Scale", "Depth Image"]
"Reference Depth", "Normal Smooth", "Smooth Weight", "Validity Threshold", "Split Threshold", "Uniform Scale", "Depth Image"]
assert inputs["Split Threshold"].default_value == 0
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.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"
@@ -265,71 +230,6 @@ def test_relief_interface_and_node_structure(relief):
validate_node_structure(obj.modifiers[0].node_group)
def test_conversion_preserves_offset_world_placement_and_initializes_inputs(surface, monkeypatch):
from anyimage.operators.convert_to_plane import object as conversion
from anyimage.common.image import image_empty_bounds
from mathutils import Matrix, Vector
obj, _, _, inputs, image = surface
group = obj.modifiers[0].node_group
monkeypatch.setattr(conversion, "load_node_group", lambda name: group)
source = bpy.data.objects.new("Offset Image", None)
source.empty_display_type = "IMAGE"
source.data = image
source.empty_display_size = 4
source.empty_image_offset = (-0.2, -0.8)
source.matrix_world = Matrix.Translation((3, -2, 5)) @ Matrix.Rotation(0.7, 4, "Y") @ Matrix.Diagonal((2, 3, 1, 1))
bpy.context.collection.objects.link(source)
bounds = image_empty_bounds(source)
matrix = source.matrix_world.copy()
material = bpy.data.materials.new("Conversion")
metadata = {"image_size": (16, 8), "reference_depth": 2.0,
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
result = conversion.create_depth_plane_object(
bpy.context, source, 2, material, image, metadata, "DEPTH")
assert result.name == "Offset Image"
assert result["anyimage_mesh_shape"] == "DEPTH_PLANE"
assert bpy.context.view_layer.objects.active == result
expected = sorted(tuple(matrix @ Vector((x, y, 0)))
for x in bounds[:2] for y in bounds[2:])
actual = sorted(tuple(result.matrix_world @ v.co) for v in result.data.vertices)
np.testing.assert_allclose(actual, expected, atol=1e-6)
center = matrix @ Vector(((bounds[0] + bounds[1]) / 2, (bounds[2] + bounds[3]) / 2, 0))
np.testing.assert_allclose(result.matrix_world.translation, center, atol=1e-6)
modifier = result.modifiers[0]
assert modifier[inputs["Thickness"].identifier] == 0
assert modifier[inputs["Depth Image"].identifier] == image
assert modifier[inputs["Valid Only"].identifier] is True
assert modifier[inputs["Reference Depth"].identifier] == pytest.approx(
2 * modifier[inputs["Uniform Scale"].identifier])
assert result.data.materials[0] == material
def test_failed_modifier_initialization_removes_partial_object(surface, monkeypatch):
from anyimage.operators.convert_to_plane import object as conversion
obj, _, _, _, image = surface
monkeypatch.setattr(conversion, "load_node_group", lambda name: obj.modifiers[0].node_group)
source = bpy.data.objects.new("Source", None)
source.empty_display_type = "IMAGE"
source.data = image
bpy.context.collection.objects.link(source)
material = bpy.data.materials.new("Unused")
before_objects, before_meshes = set(bpy.data.objects), set(bpy.data.meshes)
def fail(*args):
raise RuntimeError("input failed")
monkeypatch.setattr(conversion, "set_modifier_input", fail)
metadata = {"image_size": (16, 8), "reference_depth": 2,
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
with pytest.raises(RuntimeError, match="input failed"):
conversion.create_depth_plane_object(bpy.context, source, 2, material, image, metadata, "DEPTH")
assert set(bpy.data.objects) == before_objects
assert set(bpy.data.meshes) == before_meshes
assert material.users == 0
@pytest.mark.parametrize("threshold", [0.0, 0.4, 0.9, 1.0])
@pytest.mark.parametrize("pattern", ["valid", "invalid", "partial", "half", "stripe"])
def test_valid_only_recomputes_per_point_and_preserves_boundary_faces(surface, pattern, threshold):
@@ -400,10 +300,9 @@ def test_validity_graph_matches_reference_domains_and_connections(surface):
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"
assert len([node for node in color.id_data.nodes if node.bl_idname == "GeometryNodeImageTexture"]) == 1
consumers = [link for link in group.links if link.to_node.bl_idname != "NodeReroute"
and origin_socket(link.from_socket) == color.outputs["Color"]]
assert len(consumers) == 2
assert any(link.to_node == alpha.node for link in consumers)
assert origin_socket(delete.inputs["Geometry"].links[0].from_socket).node.node_tree.name == "O Image Plane"
@@ -430,3 +329,19 @@ 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 += ["Normal Smooth", "Smooth Weight", "Validity Threshold", "Split Threshold"]
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

@@ -1,20 +1,14 @@
"""Behavior checks for face separation and connected Depth Surface shells."""
import sys
from pathlib import Path
from collections import Counter
import bpy
import bmesh
import numpy as np
import pytest
ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(ROOT / "src"))
from node_asset_test_support import CUTOUT_BUILDERS
from depth_surface_test_support import assert_closed, component_count, evaluated, surface
from tests.support.nodes import CUTOUT_BUILDERS
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
@pytest.mark.parametrize("mode", [0, 1])

View File

@@ -1,26 +1,13 @@
import sys
from pathlib import Path
from types import SimpleNamespace
import bpy
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from anyimage.common import material
from tools.nodes.groups.image_layer import build_image_layer_group
from tests.support.materials import image_layer
from tools.nodes.groups.image_depth_layer import build_image_depth_layer_group
@pytest.fixture
def image_layer():
bpy.ops.wm.read_factory_settings(use_empty=True)
group = build_image_layer_group()
yield group
bpy.ops.wm.read_factory_settings(use_empty=True)
@pytest.mark.parametrize("image_layer", ["definition", "asset"], indirect=True)
def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
inputs = {
item.name: item for item in image_layer.interface.items_tree
@@ -61,28 +48,6 @@ def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
assert all(socket.is_linked or socket.hide for node in image_layer.nodes for socket in node.outputs)
@pytest.mark.parametrize("enabled", [False, True])
@pytest.mark.parametrize("shadeless", [False, True])
@pytest.mark.parametrize("view", ["AgX", "Unknown"])
def test_material_alpha_strength_follows_preference(image_layer, monkeypatch, enabled, shadeless, view):
monkeypatch.setattr(material, "material_node_group", lambda depth_plane=False: image_layer)
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: enabled)
color = bpy.data.images.new("Alpha edge", width=2, height=2, alpha=True)
scene = SimpleNamespace(view_settings=SimpleNamespace(view_transform=view))
result = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
layer = next(node for node in result.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
assert layer.inputs["Alpha Fix"].default_value == float(enabled)
alpha_link = layer.outputs["Alpha"].links[0]
assert alpha_link.to_node.type == ("MIX_SHADER" if shadeless else "BSDF_PRINCIPLED")
assert alpha_link.to_socket.name == ("Fac" if shadeless else "Alpha")
layer.inputs["Alpha Fix"].default_value = 0.5
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: not enabled)
second = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
second_layer = next(node for node in second.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
assert second_layer.inputs["Alpha Fix"].default_value == float(not enabled)
assert layer.inputs["Alpha Fix"].default_value == 0.5
def test_depth_layer_keeps_foreground_alpha_threshold(image_layer):
depth = build_image_depth_layer_group()
assert "Alpha Fix" not in {

View File

@@ -0,0 +1,23 @@
"""Check the saved library's public inventory and persistence."""
import bpy
from anyimage.common.node import load_node_group, node_asset_path
from tools.nodes.check import GEOMETRY_NAMES, SHADER_NAMES
def test_saved_asset_inventory():
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"]
assert [item.name for item in node_group.interface.items_tree
if item.item_type == "SOCKET" and item.in_out == "INPUT"] == expected

View File

@@ -1,8 +1,7 @@
from tests.support.paths import PROJECT_ROOT
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
@@ -10,7 +9,7 @@ class NodeAssetsTest(unittest.TestCase):
def test_geometry_assets_preserve_node_structure(self):
import bpy
from node_asset_test_support import validate_node_structure
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)
@@ -33,13 +32,6 @@ class NodeAssetsTest(unittest.TestCase):
for actual, expected in zip(group.nodes[name].location, position):
self.assertAlmostEqual(actual, expected, delta=0.01, msg=f"{group.name}: {name}")
def test_production_code_does_not_create_node_groups(self):
offenders = []
for path in SOURCE_ROOT.rglob("*.py"):
if "bpy.data.node_groups.new" in path.read_text(encoding="utf-8"):
offenders.append(path.relative_to(PROJECT_ROOT).as_posix())
self.assertEqual(offenders, [])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,9 +1,6 @@
from tests.support.paths import PROJECT_ROOT
import tomllib
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
class ToolCommandsTests(unittest.TestCase):
@@ -15,7 +12,7 @@ class ToolCommandsTests(unittest.TestCase):
project["scripts"],
{
"pack": "tools.pack:main",
"build_node": "tools.nodes.cli:main",
"build_nodes": "tools.nodes.cli:main",
"sync_models": "tools.models.cli:main",
"docs": "tools.docs:main",
},

View File

@@ -1,8 +1,8 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
from pathlib import Path
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "tools" / "docs.py"
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)

View File

@@ -1,9 +1,8 @@
from tests.support.paths import PROJECT_ROOT
import tomllib
import unittest
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MANIFEST_PATH = PROJECT_ROOT / "src" / "anyimage" / "blender_manifest.toml"

View File

@@ -1,3 +1,4 @@
from tests.support.paths import PROJECT_ROOT
import importlib.util
import tempfile
import tomllib
@@ -5,8 +6,6 @@ import unittest
from pathlib import Path
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def load_pack():
specification = importlib.util.spec_from_file_location(

View File

@@ -3,9 +3,12 @@
import bpy
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
from ..common.nodes import prepare_node_group
from ..common.nodes import prepare_node_group, float_input
from ..common.smoothing import expand_smooth
from ..common.depth_surface import project_depth_surface, thicken_depth_surface, remove_surface_attributes
from ..common.depth_surface import (
project_depth_surface, thicken_depth_surface, remove_surface_attributes,
split_depth_surface, store_surface_camera,
)
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
@@ -62,6 +65,7 @@ def build_image_depth_plane_group(image_plane_group):
validity_threshold.default_value = 0.9
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
validity_threshold.subtype = "FACTOR"
float_input(group, "Split Threshold", 0.0, maximum=1, subtype="FACTOR", parent=options)
data = group.interface.new_panel(name="Data", default_closed=True)
uniform_scale = _interface_socket(
group,
@@ -99,33 +103,6 @@ def build_image_depth_plane_group(image_plane_group):
texture.interpolation, texture.extension = "Linear", "EXTEND"
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
links.new(texture_input.outputs["Depth Image"], texture.inputs["Image"])
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
surface = project_depth_surface(
group, plane.outputs["Geometry"], texture.outputs["Color"],
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
depth_input.outputs["Depth Scale"],
)
shell_input = _group_input(nodes, {"Thickness", "Normal Smooth"})
shell = thicken_depth_surface(
group, surface, shell_input.outputs["Thickness"],
normal_smooth=shell_input.outputs["Normal Smooth"],
)
positive = nodes.new("ShaderNodeMath")
positive.operation = "GREATER_THAN"
links.new(shell_input.outputs["Thickness"], positive.inputs[0])
positive.inputs[1].default_value = 0
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(positive.outputs[0], choose.inputs["Switch"])
links.new(surface, choose.inputs["False"])
links.new(shell, choose.inputs["True"])
clean = remove_surface_attributes(group, choose.outputs["Output"])
# Preserve the repeated cap input independently of its display routing.
cap = nodes.new("NodeReroute")
links.new(nodes["Store Named Attribute.001"].outputs["Geometry"], cap.inputs[0])
links.new(cap.outputs[0], nodes["Join Geometry"].inputs["Geometry"])
projection_link = next(link for link in links if link.from_socket == plane.outputs["Geometry"])
projection_input = projection_link.to_socket
threshold_input = _group_input(nodes, {"Validity Threshold"})
channels = nodes.new("FunctionNodeSeparateColor")
channels.mode = "RGB"
@@ -147,7 +124,35 @@ def build_image_depth_plane_group(image_plane_group):
links.new(cull_controls.outputs["Valid Only"], cull.inputs["Switch"])
links.new(delete.outputs["Geometry"], cull.inputs["True"])
links.new(plane.outputs["Geometry"], cull.inputs["False"])
links.new(cull.outputs["Output"], projection_input)
split_input = _group_input(nodes, {"Split Threshold"})
geometry = split_depth_surface(
group, cull.outputs["Output"], texture_input.outputs["Depth Image"],
split_input.outputs["Split Threshold"], scale_input.outputs["Uniform Scale"],
)
geometry, camera = store_surface_camera(
group, geometry, texture.outputs["Color"], split_input.outputs["Split Threshold"],
)
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
surface = project_depth_surface(
group, geometry, camera,
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
depth_input.outputs["Depth Scale"],
)
shell_input = _group_input(nodes, {"Thickness", "Normal Smooth"})
shell = thicken_depth_surface(
group, surface, shell_input.outputs["Thickness"],
normal_smooth=shell_input.outputs["Normal Smooth"],
)
positive = nodes.new("ShaderNodeMath")
positive.operation = "GREATER_THAN"
links.new(shell_input.outputs["Thickness"], positive.inputs[0])
positive.inputs[1].default_value = 0
choose = nodes.new("GeometryNodeSwitch")
choose.input_type = "GEOMETRY"
links.new(positive.outputs[0], choose.inputs["Switch"])
links.new(surface, choose.inputs["False"])
links.new(shell, choose.inputs["True"])
clean = remove_surface_attributes(group, choose.outputs["Output"])
geometry = expand_smooth(
group, clean, iterations_name="Smooth", weight_name="Smooth Weight",
)