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:
@@ -47,7 +47,7 @@ uv run pytest
|
|||||||
uv run pack
|
uv run pack
|
||||||
|
|
||||||
# 构建并验证 Blender 节点资产
|
# 构建并验证 Blender 节点资产
|
||||||
uv run build_node
|
uv run build_nodes
|
||||||
|
|
||||||
# 准备、校验并同步 ONNX 模型到 R2
|
# 准备、校验并同步 ONNX 模型到 R2
|
||||||
uv run sync_models
|
uv run sync_models
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Blender Stub 共用 `blender_test_support.py`;Server 和实际 Blender 几何
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `uv run pack` | 生成 Windows、macOS arm64、Linux 扩展包 |
|
| `uv run pack` | 生成 Windows、macOS arm64、Linux 扩展包 |
|
||||||
| `uv run pack --prepare` | 为本机同步扩展 wheel 与 Manifest |
|
| `uv run pack --prepare` | 为本机同步扩展 wheel 与 Manifest |
|
||||||
| `uv run build_node` | 构建、验证节点资产并运行测试 |
|
| `uv run build_nodes` | 构建、验证节点资产并运行测试 |
|
||||||
| `uv run sync_models` | 准备、校验并上传模型声明的文件 |
|
| `uv run sync_models` | 准备、校验并上传模型声明的文件 |
|
||||||
| `uv run docs -dev` | 本地预览文档 |
|
| `uv run docs -dev` | 本地预览文档 |
|
||||||
| `uv run docs` | 通过 Mike 发布项目版本并更新 latest |
|
| `uv run docs` | 通过 Mike 发布项目版本并更新 latest |
|
||||||
|
|||||||
@@ -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`
|
节点组文件在 `tools/nodes/groups`
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
保存后,`check.py` 在独立 Blender 进程中重新加载资产并做基本几何求值。接口、默认值和详细几何行为由 `tests/` 检查。
|
保存后,`check.py` 在独立 Blender 进程中重新加载资产并做基本几何求值。接口、默认值和详细几何行为由 `tests/` 检查。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run build_node --blender "/path/to/blender"
|
uv run build_nodes --blender "/path/to/blender"
|
||||||
uv run build_node --skip-tests
|
uv run build_nodes --skip-tests
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本按 `--blender`、环境变量 `BLENDER_BIN`、项目 `.env`、`PATH` 的顺序查找 Blender。无法自动找到时,在项目根目录的 `.env` 中配置:
|
脚本按 `--blender`、环境变量 `BLENDER_BIN`、项目 `.env`、`PATH` 的顺序查找 Blender。无法自动找到时,在项目根目录的 `.env` 中配置:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ dev = [
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
pack = "tools.pack:main"
|
pack = "tools.pack:main"
|
||||||
build_node = "tools.nodes.cli:main"
|
build_nodes = "tools.nodes.cli:main"
|
||||||
sync_models = "tools.models.cli:main"
|
sync_models = "tools.models.cli:main"
|
||||||
docs = "tools.docs:main"
|
docs = "tools.docs:main"
|
||||||
|
|
||||||
@@ -37,3 +37,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["tools"]
|
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
0
tests/__init__.py
Normal file
0
tests/anyimage/__init__.py
Normal file
0
tests/anyimage/__init__.py
Normal file
0
tests/anyimage/common/__init__.py
Normal file
0
tests/anyimage/common/__init__.py
Normal file
@@ -1,16 +1,11 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
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():
|
def load_common_modules():
|
||||||
fake_bpy = ModuleType("bpy")
|
fake_bpy = ModuleType("bpy")
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
@@ -7,7 +5,6 @@ import numpy as np
|
|||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common import image as images
|
from anyimage.common import image as images
|
||||||
from anyimage.operators.convert_to_plane.object import create_color_image
|
from anyimage.operators.convert_to_plane.object import create_color_image
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import numpy as np
|
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.selection import SelectionMask
|
||||||
from anyimage.common.depth import (
|
from anyimage.common.depth import (
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common import image as images
|
from anyimage.common import image as images
|
||||||
|
|
||||||
@@ -2,8 +2,7 @@ import tempfile
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from common_test_support import load_common_modules
|
from tests.anyimage.common.support import load_common_modules
|
||||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
class ImageDataTest(unittest.TestCase):
|
class ImageDataTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -3,23 +3,11 @@ from pathlib import Path
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
from tests.support.blender import BlenderTestCase
|
||||||
from blender_test_support import BlenderTestCase
|
|
||||||
|
|
||||||
|
|
||||||
class ImageEditToolTest(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):
|
def test_packed_image_export_does_not_use_scene_render_transform(self):
|
||||||
@@ -222,12 +210,3 @@ class ImageEditToolTest(BlenderTestCase):
|
|||||||
"Select an active Image Empty",
|
"Select an active Image Empty",
|
||||||
):
|
):
|
||||||
self.image_interaction.active_image_edit_source(context)
|
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, [])
|
|
||||||
@@ -1,21 +1,14 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
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 image as image_data
|
||||||
from anyimage.common import material as material_data
|
from anyimage.common import material as material_data
|
||||||
from anyimage.operators.convert_to_plane import object as plane_object
|
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 object as cutout_object
|
||||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
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():
|
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||||
27
tests/anyimage/common/test_material_alpha.py
Normal file
27
tests/anyimage/common/test_material_alpha.py
Normal 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
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from common_test_support import load_common_modules
|
from tests.anyimage.common.support import load_common_modules
|
||||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
class MaterialSettingsTest(unittest.TestCase):
|
class MaterialSettingsTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
@@ -7,7 +5,6 @@ import numpy as np
|
|||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage import preferences
|
from anyimage import preferences
|
||||||
from anyimage.common import image as images
|
from anyimage.common import image as images
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from common_test_support import load_common_modules
|
from tests.anyimage.common.support import load_common_modules
|
||||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
class ModifierInputsTest(unittest.TestCase):
|
class ModifierInputsTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from tests.anyimage.common.support import load_common_modules
|
||||||
from common_test_support import load_common_modules
|
|
||||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
class NodeLoadingTest(unittest.TestCase):
|
class NodeLoadingTest(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
|
||||||
sys.path.insert(0, str(SOURCE_ROOT))
|
|
||||||
|
|
||||||
from anyimage.common import selection
|
from anyimage.common import selection
|
||||||
|
|
||||||
0
tests/anyimage/operators/__init__.py
Normal file
0
tests/anyimage/operators/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class AiImageOperatorsTest(BlenderTestCase):
|
class AiImageOperatorsTest(BlenderTestCase):
|
||||||
@@ -3,7 +3,7 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class AiSetupTest(BlenderTestCase):
|
class AiSetupTest(BlenderTestCase):
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
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 import geometry as cutout_geometry
|
||||||
from anyimage.operators.cutout_tool.geometry import poisson_balloon_profile
|
from anyimage.operators.cutout_tool.geometry import poisson_balloon_profile
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common import selection, viewport
|
from anyimage.common import selection, viewport
|
||||||
from anyimage.operators import mask_tool as mask
|
from anyimage.operators import mask_tool as mask
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib
|
import importlib
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from blender_test_support import mock_bpy_imports
|
from tests.support.blender import mock_bpy_imports
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
||||||
|
|
||||||
|
|
||||||
class ClipboardImageTest(unittest.TestCase):
|
class ClipboardImageTest(unittest.TestCase):
|
||||||
34
tests/anyimage/operators/test_cutout_depth.py
Normal file
34
tests/anyimage/operators/test_cutout_depth.py
Normal 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)
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from scipy.sparse import coo_matrix
|
from scipy.sparse import coo_matrix
|
||||||
from scipy.sparse.csgraph import connected_components
|
from scipy.sparse.csgraph import connected_components
|
||||||
from scipy.spatial import cKDTree
|
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 geometry as g
|
||||||
from anyimage.operators.cutout_tool import fine_mesh as fine
|
from anyimage.operators.cutout_tool import fine_mesh as fine
|
||||||
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
import unittest
|
import unittest
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
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.common.selection import SelectionPath, rasterize_selection_path
|
||||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
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.selection import SelectionPath
|
||||||
from anyimage.common import viewport
|
from anyimage.common import viewport
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
@@ -7,9 +5,6 @@ import numpy as np
|
|||||||
import pytest
|
import pytest
|
||||||
from mathutils import Matrix, Vector
|
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 depth as depth_data
|
||||||
from anyimage.common import image as image_data
|
from anyimage.common import image as image_data
|
||||||
@@ -24,7 +19,7 @@ from anyimage.server.geometry.depth_texture import (
|
|||||||
write_depth_texture,
|
write_depth_texture,
|
||||||
)
|
)
|
||||||
from anyimage.server.models.geometry import GeometryFrame
|
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():
|
def test_object_normal_image_follows_cutout_inward_axis():
|
||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class CutoutOperatorsTest(BlenderTestCase):
|
class CutoutOperatorsTest(BlenderTestCase):
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common.image import premultiplied_rgba
|
from anyimage.common.image import premultiplied_rgba
|
||||||
from anyimage.operators import frame_tool as frame
|
from anyimage.operators import frame_tool as frame
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common.selection import clip_polygon_halfplanes
|
from anyimage.common.selection import clip_polygon_halfplanes
|
||||||
from anyimage.operators import frame_tool as frame
|
from anyimage.operators import frame_tool as frame
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class FrameToolTest(BlenderTestCase):
|
class FrameToolTest(BlenderTestCase):
|
||||||
@@ -1,14 +1,8 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Matrix
|
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 import image as image_data
|
||||||
from anyimage.common.selection import SelectionMask
|
from anyimage.common.selection import SelectionMask
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class MaskToolTest(BlenderTestCase):
|
class MaskToolTest(BlenderTestCase):
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common import image, viewport
|
from anyimage.common import image, viewport
|
||||||
from anyimage.operators import mask_tool as mask
|
from anyimage.operators import mask_tool as mask
|
||||||
69
tests/anyimage/operators/test_plane_conversion.py
Normal file
69
tests/anyimage/operators/test_plane_conversion.py
Normal 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
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mathutils import Matrix, Vector
|
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 image as image_data
|
||||||
from anyimage.common import object as object_data
|
from anyimage.common import object as object_data
|
||||||
from anyimage.operators.clipboard_image import actions as clipboard_actions
|
from anyimage.operators.clipboard_image import actions as clipboard_actions
|
||||||
from anyimage.operators.convert_to_plane import object as plane_object
|
from anyimage.operators.convert_to_plane import object as plane_object
|
||||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
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_plane import build_image_plane_group
|
||||||
from tools.nodes.groups.image_depth_plane import build_image_depth_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 tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class PlaneOperatorsTest(BlenderTestCase):
|
class PlaneOperatorsTest(BlenderTestCase):
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class RectifyToolTest(BlenderTestCase):
|
class RectifyToolTest(BlenderTestCase):
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
from anyimage.operators.upscale import UpscaleImage
|
from anyimage.operators.upscale import UpscaleImage
|
||||||
from anyimage.common.image import image_pixels
|
from anyimage.common.image import image_pixels
|
||||||
|
|
||||||
0
tests/anyimage/server/__init__.py
Normal file
0
tests/anyimage/server/__init__.py
Normal file
@@ -1,3 +1,4 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -6,8 +7,6 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
def load_server_module():
|
def load_server_module():
|
||||||
server_directory = PROJECT_ROOT / "src" / "anyimage" / "server"
|
server_directory = PROJECT_ROOT / "src" / "anyimage" / "server"
|
||||||
specification = importlib.util.spec_from_file_location(
|
specification = importlib.util.spec_from_file_location(
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_server_module(name):
|
def load_server_module(name):
|
||||||
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
||||||
sys.path.insert(0, str(addon_directory))
|
sys.path.insert(0, str(addon_directory))
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
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 import prediction_artifacts as artifacts
|
||||||
from server.geometry.edge_depth import extend_edge_depth
|
from server.geometry.edge_depth import extend_edge_depth
|
||||||
@@ -3,7 +3,7 @@ import json
|
|||||||
import sys
|
import sys
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from server_runtime_test_support import ServerTestCase
|
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||||
|
|
||||||
class GeometryArtifactsTest(ServerTestCase):
|
class GeometryArtifactsTest(ServerTestCase):
|
||||||
def test_depth_alpha_multiplies_coverage_without_changing_geometry(self):
|
def test_depth_alpha_multiplies_coverage_without_changing_geometry(self):
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from server_runtime_test_support import ServerTestCase
|
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||||
|
|
||||||
class JobServerTest(ServerTestCase):
|
class JobServerTest(ServerTestCase):
|
||||||
def test_dedicated_server_runs_only_one_job_at_a_time(self):
|
def test_dedicated_server_runs_only_one_job_at_a_time(self):
|
||||||
@@ -171,4 +168,3 @@ class JobServerTest(ServerTestCase):
|
|||||||
"generate-cutout-artifacts",
|
"generate-cutout-artifacts",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
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):
|
class MediaInputTest(unittest.TestCase):
|
||||||
@@ -1,22 +1,11 @@
|
|||||||
import sys
|
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
SOURCE_DIR = (
|
from server.models import onnx_ben2
|
||||||
Path(__file__).resolve().parents[1]
|
|
||||||
/ "src"
|
|
||||||
/ "anyimage"
|
|
||||||
/ "server"
|
|
||||||
/ "models"
|
|
||||||
)
|
|
||||||
sys.path.insert(0, str(SOURCE_DIR))
|
|
||||||
|
|
||||||
import onnx_ben2
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSession:
|
class FakeSession:
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_module(name, relative_path):
|
def load_module(name, relative_path):
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
@@ -7,9 +8,6 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_download_module():
|
def load_download_module():
|
||||||
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
||||||
sys.path.insert(0, str(addon_directory))
|
sys.path.insert(0, str(addon_directory))
|
||||||
@@ -5,7 +5,7 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
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):
|
class ModelDownloadJobsTest(unittest.TestCase):
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
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 import moge2 as moge2_model
|
||||||
from anyimage.server.models.geometry import GeometryFrame
|
from anyimage.server.models.geometry import GeometryFrame
|
||||||
@@ -6,7 +6,7 @@ from unittest.mock import patch
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
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):
|
class ModelInferenceTest(unittest.TestCase):
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from server_runtime_test_support import ServerTestCase
|
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||||
|
|
||||||
class ModelManagerTest(ServerTestCase):
|
class ModelManagerTest(ServerTestCase):
|
||||||
def test_required_model_download_skips_ready_model(self):
|
def test_required_model_download_skips_ready_model(self):
|
||||||
@@ -182,5 +180,3 @@ class ModelManagerTest(ServerTestCase):
|
|||||||
lambda: cache.get_upscale("upscale", "cuda"),
|
lambda: cache.get_upscale("upscale", "cuda"),
|
||||||
"release_upscale",
|
"release_upscale",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
@@ -7,9 +8,6 @@ from unittest.mock import patch
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_module():
|
def load_module():
|
||||||
anyimage = ModuleType("anyimage")
|
anyimage = ModuleType("anyimage")
|
||||||
anyimage.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
|
anyimage.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
SOURCE_DIR = (
|
from server.models import onnx_runtime
|
||||||
Path(__file__).resolve().parents[1]
|
|
||||||
/ "src"
|
|
||||||
/ "anyimage"
|
|
||||||
/ "server"
|
|
||||||
/ "models"
|
|
||||||
)
|
|
||||||
sys.path.insert(0, str(SOURCE_DIR))
|
|
||||||
|
|
||||||
import onnx_runtime
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSession:
|
class FakeSession:
|
||||||
@@ -211,6 +201,5 @@ class OnnxRuntimeTest(unittest.TestCase):
|
|||||||
self.assertEqual(providers, ["CPUExecutionProvider"])
|
self.assertEqual(providers, ["CPUExecutionProvider"])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
from PIL import Image
|
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.models import onnx_pisa_sr as pisa
|
||||||
from server.model_manager import ModelManager, ModelSessionCache
|
from server.model_manager import ModelManager, ModelSessionCache
|
||||||
from server import model_catalog
|
from server import model_catalog
|
||||||
@@ -1,22 +1,11 @@
|
|||||||
import sys
|
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
SOURCE_DIR = (
|
from server.models import onnx_upscale
|
||||||
Path(__file__).resolve().parents[1]
|
|
||||||
/ "src"
|
|
||||||
/ "anyimage"
|
|
||||||
/ "server"
|
|
||||||
/ "models"
|
|
||||||
)
|
|
||||||
sys.path.insert(0, str(SOURCE_DIR))
|
|
||||||
|
|
||||||
import onnx_upscale
|
|
||||||
|
|
||||||
|
|
||||||
class FakeSession:
|
class FakeSession:
|
||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import numpy as np
|
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):
|
class ServerJobsTest(unittest.TestCase):
|
||||||
@@ -2,7 +2,7 @@ import importlib
|
|||||||
import sys
|
import sys
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class AddonRegistrationTest(BlenderTestCase):
|
class AddonRegistrationTest(BlenderTestCase):
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class ImageMenuTest(BlenderTestCase):
|
class ImageMenuTest(BlenderTestCase):
|
||||||
@@ -94,8 +94,6 @@ class ImageMenuTest(BlenderTestCase):
|
|||||||
self.assertNotIn(("anyimage.setup_ai_environment", True), calls)
|
self.assertNotIn(("anyimage.setup_ai_environment", True), calls)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_image_operators_stay_clickable_until_server_is_busy(self):
|
def test_image_operators_stay_clickable_until_server_is_busy(self):
|
||||||
context = SimpleNamespace(
|
context = SimpleNamespace(
|
||||||
object=SimpleNamespace(data=SimpleNamespace()),
|
object=SimpleNamespace(data=SimpleNamespace()),
|
||||||
@@ -6,7 +6,7 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class JobOperatorTest(BlenderTestCase):
|
class JobOperatorTest(BlenderTestCase):
|
||||||
@@ -2,7 +2,7 @@ import importlib
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class PreferencesTest(BlenderTestCase):
|
class PreferencesTest(BlenderTestCase):
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import importlib
|
import importlib
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from blender_test_support import BlenderTestCase
|
from tests.support.blender import BlenderTestCase
|
||||||
|
|
||||||
|
|
||||||
class ServerPanelTest(BlenderTestCase):
|
class ServerPanelTest(BlenderTestCase):
|
||||||
@@ -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
|
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")]
|
||||||
|
|||||||
0
tests/support/__init__.py
Normal file
0
tests/support/__init__.py
Normal file
@@ -1,12 +1,11 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib
|
import importlib
|
||||||
import builtins
|
import builtins
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from types import ModuleType, SimpleNamespace
|
from types import ModuleType, SimpleNamespace
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||||
|
|
||||||
|
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[1]
|
from tests.support.nodes import CUTOUT_BUILDERS
|
||||||
|
|
||||||
sys.path.insert(0, str(ROOT / "src"))
|
|
||||||
|
|
||||||
|
|
||||||
from node_asset_test_support import CUTOUT_BUILDERS
|
|
||||||
|
|
||||||
|
|
||||||
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
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 image as image_data
|
||||||
from anyimage.common import object as object_data
|
from anyimage.common import object as object_data
|
||||||
from anyimage.common.selection import SelectionMask
|
from anyimage.common.selection import SelectionMask
|
||||||
17
tests/support/materials.py
Normal file
17
tests/support/materials.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import bpy
|
||||||
|
import pytest
|
||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
|
from tools.nodes.groups.image_layer import build_image_layer_group
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def image_layer(request):
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
if getattr(request, "param", "definition") == "asset":
|
||||||
|
with bpy.data.libraries.load(str(PROJECT_ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (_, target):
|
||||||
|
target.node_groups = ["O Image Layer"]
|
||||||
|
group = target.node_groups[0]
|
||||||
|
else:
|
||||||
|
group = build_image_layer_group()
|
||||||
|
yield group
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
4
tests/support/paths.py
Normal file
4
tests/support/paths.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
"""Resolve project paths independently of test directory depth."""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
82
tests/support/planes.py
Normal file
82
tests/support/planes.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Evaluate complete-image projection and rectangular surface topology."""
|
||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = PROJECT_ROOT
|
||||||
|
from tools.nodes.groups.image_plane import build_image_plane_group
|
||||||
|
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
|
||||||
|
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=("definition", "asset"))
|
||||||
|
def surface(request):
|
||||||
|
return create_surface(request.param, "DEPTH")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=("definition", "asset"))
|
||||||
|
def relief(request):
|
||||||
|
return create_surface(request.param, "RELIEF")
|
||||||
|
|
||||||
|
|
||||||
|
def create_surface(source, plane_type):
|
||||||
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||||
|
group_name = f"O Image {plane_type.title()} Plane"
|
||||||
|
if source == "asset":
|
||||||
|
with bpy.data.libraries.load(str(ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (library, target):
|
||||||
|
target.node_groups = [group_name]
|
||||||
|
group = target.node_groups[0]
|
||||||
|
plane = group.nodes["Group"].node_tree
|
||||||
|
else:
|
||||||
|
plane = build_image_plane_group()
|
||||||
|
builder = build_image_depth_plane_group if plane_type == "DEPTH" else build_image_relief_plane_group
|
||||||
|
group = builder(plane)
|
||||||
|
mesh = bpy.data.meshes.new("Rectangle")
|
||||||
|
mesh.from_pydata([(-2, -1, 0), (2, -1, 0), (2, 1, 0), (-2, 1, 0)], [], [(0, 1, 2, 3)])
|
||||||
|
material = bpy.data.materials.new("Image")
|
||||||
|
mesh.materials.append(material)
|
||||||
|
obj = bpy.data.objects.new("Surface", mesh)
|
||||||
|
bpy.context.collection.objects.link(obj)
|
||||||
|
modifier = obj.modifiers.new("Surface", "NODES")
|
||||||
|
modifier.node_group = group
|
||||||
|
image = bpy.data.images.new("Camera", width=16, height=8, float_buffer=True)
|
||||||
|
image.colorspace_settings.name = "Non-Color"
|
||||||
|
image.alpha_mode = "CHANNEL_PACKED"
|
||||||
|
yy, xx = np.mgrid[:8, :16]
|
||||||
|
# Camera principal point is deliberately off center; Y points down.
|
||||||
|
pixels = np.ones((8, 16, 4), dtype=np.float32)
|
||||||
|
pixels[..., 0] = ((xx + 0.5) - 5) / 4
|
||||||
|
pixels[..., 1] = (3 - (yy + 0.5)) / 4
|
||||||
|
pixels[..., 2] = 2 + (xx + 0.5) / 16
|
||||||
|
pixels[..., 3] = 1
|
||||||
|
image.pixels.foreach_set(pixels.ravel())
|
||||||
|
inputs = {item.name: item for item in group.interface.items_tree
|
||||||
|
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
|
||||||
|
def set_value(name, value):
|
||||||
|
modifier[inputs[name].identifier] = value
|
||||||
|
obj.update_tag(refresh={"DATA"})
|
||||||
|
bpy.context.view_layer.update()
|
||||||
|
|
||||||
|
for name, value in (("Subdivide", 2), ("Depth Image", image),
|
||||||
|
("Uniform Scale", 0.5), ("Reference Depth", 1.2),
|
||||||
|
("Depth Scale", 0.0), ("Thickness", 0.0)):
|
||||||
|
set_value(name, value)
|
||||||
|
return obj, set_value, plane, inputs, image
|
||||||
|
|
||||||
|
|
||||||
|
def evaluated(obj):
|
||||||
|
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||||
|
mesh = result.to_mesh()
|
||||||
|
try:
|
||||||
|
return (
|
||||||
|
np.array([v.co[:] for v in mesh.vertices]),
|
||||||
|
[tuple(face.vertices) for face in mesh.polygons],
|
||||||
|
np.array([value.uv[:] for value in mesh.uv_layers["UVMap"].data]) if mesh.uv_layers else np.empty((0, 2)),
|
||||||
|
[material.name if material else None for material in mesh.materials],
|
||||||
|
[face.material_index for face in mesh.polygons],
|
||||||
|
[attr.name for attr in mesh.attributes],
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
result.to_mesh_clear()
|
||||||
@@ -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
0
tests/tools/__init__.py
Normal file
0
tests/tools/models/__init__.py
Normal file
0
tests/tools/models/__init__.py
Normal file
@@ -1,13 +1,10 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_sync_models():
|
def load_sync_models():
|
||||||
path = PROJECT_ROOT / "tools" / "models" / "cli.py"
|
path = PROJECT_ROOT / "tools" / "models" / "cli.py"
|
||||||
spec = importlib.util.spec_from_file_location("sync_models", path)
|
spec = importlib.util.spec_from_file_location("sync_models", path)
|
||||||
0
tests/tools/nodes/__init__.py
Normal file
0
tests/tools/nodes/__init__.py
Normal file
@@ -8,7 +8,7 @@ import pytest
|
|||||||
from tools.nodes.groups.image_plane import build_image_plane_group
|
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_depth_plane import build_image_depth_plane_group
|
||||||
from tools.nodes.groups.image_relief_plane import build_image_relief_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.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.flow import geometry_paths, origin_socket, peer_links
|
||||||
from tools.nodes.arrangement.dimensions import RULES, estimate_node_dimensions
|
from tools.nodes.arrangement.dimensions import RULES, estimate_node_dimensions
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
SCRIPT_PATH = PROJECT_ROOT / "tools" / "nodes" / "cli.py"
|
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 = []
|
calls = []
|
||||||
monkeypatch.setattr(launcher, "blender_path", lambda _: executable)
|
monkeypatch.setattr(launcher, "blender_path", lambda _: executable)
|
||||||
monkeypatch.setattr(launcher, "run", calls.append)
|
monkeypatch.setattr(launcher, "run", calls.append)
|
||||||
monkeypatch.setattr("sys.argv", ["build_node"])
|
monkeypatch.setattr("sys.argv", ["build_nodes"])
|
||||||
|
|
||||||
launcher.main()
|
launcher.main()
|
||||||
|
|
||||||
@@ -3,17 +3,20 @@ from collections import Counter
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
import numpy as np
|
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.operators.cutout_tool.object import load_cutout_node_group
|
||||||
|
|
||||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
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)
|
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 groups["FLAT"] == groups["SOLID"]
|
||||||
assert len(set(groups.values())) == 3
|
assert len(set(groups.values())) == 3
|
||||||
for group in set(groups.values()):
|
for group in set(groups.values()):
|
||||||
@@ -80,4 +83,3 @@ def test_saved_cutout_defaults():
|
|||||||
assert set(uses.values()) == {2}
|
assert set(uses.values()) == {2}
|
||||||
finally:
|
finally:
|
||||||
evaluated.to_mesh_clear()
|
evaluated.to_mesh_clear()
|
||||||
print("Validated three Cutout assets, four presets, both thickness modes and closed defaults")
|
|
||||||
@@ -1,70 +1,17 @@
|
|||||||
"""Contracts for smooth depth volume, editable presets and normal masks."""
|
"""Contracts for smooth depth volume, editable presets and normal masks."""
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
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_fields
|
||||||
from tools.nodes.groups.image_depth_cutout import build_volume_rounding
|
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
|
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])
|
@pytest.mark.parametrize("iterations", [0, 50])
|
||||||
def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
|
def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
|
||||||
from scipy.spatial import cKDTree
|
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)
|
obj, set_value = surface(step=0, resolution=4)
|
||||||
image = bpy.data.images["Camera"]
|
image = bpy.data.images["Camera"]
|
||||||
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
|
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():
|
def test_balloon_shell_converges_to_front_with_thickness():
|
||||||
from scipy.spatial import cKDTree
|
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)
|
obj, set_value = surface(step=1, resolution=10)
|
||||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
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])
|
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():
|
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()
|
obj, set_value = surface()
|
||||||
set_value("Thickness", 0.3)
|
set_value("Thickness", 0.3)
|
||||||
bpy.context.view_layer.update()
|
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("structure", [0, 1, 4])
|
||||||
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
|
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
|
||||||
def test_uniform_front_inflation_scales_with_distance(thickness, structure):
|
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)
|
obj, set_value = surface(step=0, resolution=10)
|
||||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
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])
|
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():
|
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)
|
obj, set_value = surface(step=1, resolution=10)
|
||||||
set_value("Front Inflation", 1.0)
|
set_value("Front Inflation", 1.0)
|
||||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
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():
|
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)
|
obj, set_value = surface(step=0, resolution=10)
|
||||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
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])
|
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||||
@@ -1,90 +1,15 @@
|
|||||||
"""Evaluate complete-image projection and rectangular surface topology."""
|
"""Evaluate complete-image projection and rectangular surface topology."""
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[1]
|
from tests.support.planes import surface, relief, evaluated
|
||||||
from tools.nodes.groups.image_plane import build_image_plane_group
|
from tests.support.nodes import validate_node_structure
|
||||||
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 tools.nodes.arrangement.flow import origin_socket
|
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])
|
@pytest.mark.parametrize("subdivide", [0, 1, 3])
|
||||||
def test_zero_depth_matches_plane_geometry_and_uv(surface, subdivide):
|
def test_zero_depth_matches_plane_geometry_and_uv(surface, subdivide):
|
||||||
obj, set_value, plane, inputs, _ = surface
|
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)
|
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):
|
def test_depth_jump_keeps_rectangular_faces(surface):
|
||||||
obj, set_value, _, _, image = surface
|
obj, set_value, _, _, image = surface
|
||||||
base = evaluated(obj)
|
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):
|
def test_surface_interface_and_node_structure(surface):
|
||||||
obj, _, _, inputs, _ = surface
|
obj, _, _, inputs, _ = surface
|
||||||
assert list(inputs) == ["Geometry", "Subdivide", "Thickness", "Depth Scale", "Smooth", "Valid Only",
|
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"].default_value == pytest.approx(0.9)
|
||||||
assert inputs["Validity Threshold"].parent.name == "Options"
|
assert inputs["Validity Threshold"].parent.name == "Options"
|
||||||
assert inputs["Validity Threshold"].subtype == "FACTOR"
|
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)
|
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("threshold", [0.0, 0.4, 0.9, 1.0])
|
||||||
@pytest.mark.parametrize("pattern", ["valid", "invalid", "partial", "half", "stripe"])
|
@pytest.mark.parametrize("pattern", ["valid", "invalid", "partial", "half", "stripe"])
|
||||||
def test_valid_only_recomputes_per_point_and_preserves_boundary_faces(surface, pattern, threshold):
|
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"
|
assert alpha.name == "Alpha" and alpha.node.bl_idname == "FunctionNodeSeparateColor"
|
||||||
color = origin_socket(alpha.node.inputs["Color"].links[0].from_socket).node
|
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 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"
|
consumers = [link for link in group.links if link.to_node.bl_idname != "NodeReroute"
|
||||||
and origin_socket(link.from_socket) == color.outputs["Color"]]
|
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"
|
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 Weight", 1.0)
|
||||||
set_value("Smooth", 0)
|
set_value("Smooth", 0)
|
||||||
np.testing.assert_allclose(evaluated(obj)[0], base[0], atol=1e-6)
|
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)
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import bpy
|
import bpy
|
||||||
import bmesh
|
import bmesh
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
ROOT = Path(__file__).parents[1]
|
|
||||||
|
|
||||||
sys.path.insert(0, str(ROOT / "src"))
|
from tests.support.nodes import CUTOUT_BUILDERS
|
||||||
|
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
|
||||||
|
|
||||||
from node_asset_test_support import CUTOUT_BUILDERS
|
|
||||||
from depth_surface_test_support import assert_closed, component_count, evaluated, surface
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("mode", [0, 1])
|
@pytest.mark.parametrize("mode", [0, 1])
|
||||||
@@ -1,26 +1,13 @@
|
|||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
|
||||||
|
|
||||||
from anyimage.common import material
|
from tests.support.materials import image_layer
|
||||||
from tools.nodes.groups.image_layer import build_image_layer_group
|
|
||||||
from tools.nodes.groups.image_depth_layer import build_image_depth_layer_group
|
from tools.nodes.groups.image_depth_layer import build_image_depth_layer_group
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.mark.parametrize("image_layer", ["definition", "asset"], indirect=True)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
|
def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
|
||||||
inputs = {
|
inputs = {
|
||||||
item.name: item for item in image_layer.interface.items_tree
|
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)
|
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):
|
def test_depth_layer_keeps_foreground_alpha_threshold(image_layer):
|
||||||
depth = build_image_depth_layer_group()
|
depth = build_image_depth_layer_group()
|
||||||
assert "Alpha Fix" not in {
|
assert "Alpha Fix" not in {
|
||||||
23
tests/tools/nodes/test_inventory.py
Normal file
23
tests/tools/nodes/test_inventory.py
Normal 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
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
|
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +9,7 @@ class NodeAssetsTest(unittest.TestCase):
|
|||||||
def test_geometry_assets_preserve_node_structure(self):
|
def test_geometry_assets_preserve_node_structure(self):
|
||||||
import bpy
|
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
|
from tools.nodes.arrangement.arrange import arrange_nodes
|
||||||
|
|
||||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
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):
|
for actual, expected in zip(group.nodes[name].location, position):
|
||||||
self.assertAlmostEqual(actual, expected, delta=0.01, msg=f"{group.name}: {name}")
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import tomllib
|
import tomllib
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
class ToolCommandsTests(unittest.TestCase):
|
class ToolCommandsTests(unittest.TestCase):
|
||||||
@@ -15,7 +12,7 @@ class ToolCommandsTests(unittest.TestCase):
|
|||||||
project["scripts"],
|
project["scripts"],
|
||||||
{
|
{
|
||||||
"pack": "tools.pack:main",
|
"pack": "tools.pack:main",
|
||||||
"build_node": "tools.nodes.cli:main",
|
"build_nodes": "tools.nodes.cli:main",
|
||||||
"sync_models": "tools.models.cli:main",
|
"sync_models": "tools.models.cli:main",
|
||||||
"docs": "tools.docs:main",
|
"docs": "tools.docs:main",
|
||||||
},
|
},
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
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)
|
SPEC = importlib.util.spec_from_file_location("docs_script", SCRIPT_PATH)
|
||||||
docs = importlib.util.module_from_spec(SPEC)
|
docs = importlib.util.module_from_spec(SPEC)
|
||||||
SPEC.loader.exec_module(docs)
|
SPEC.loader.exec_module(docs)
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import tomllib
|
import tomllib
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
MANIFEST_PATH = PROJECT_ROOT / "src" / "anyimage" / "blender_manifest.toml"
|
MANIFEST_PATH = PROJECT_ROOT / "src" / "anyimage" / "blender_manifest.toml"
|
||||||
|
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from tests.support.paths import PROJECT_ROOT
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import tempfile
|
import tempfile
|
||||||
import tomllib
|
import tomllib
|
||||||
@@ -5,8 +6,6 @@ import unittest
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
|
|
||||||
|
|
||||||
def load_pack():
|
def load_pack():
|
||||||
specification = importlib.util.spec_from_file_location(
|
specification = importlib.util.spec_from_file_location(
|
||||||
@@ -3,9 +3,12 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
|
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.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"
|
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.default_value = 0.9
|
||||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
||||||
validity_threshold.subtype = "FACTOR"
|
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)
|
data = group.interface.new_panel(name="Data", default_closed=True)
|
||||||
uniform_scale = _interface_socket(
|
uniform_scale = _interface_socket(
|
||||||
group,
|
group,
|
||||||
@@ -99,33 +103,6 @@ def build_image_depth_plane_group(image_plane_group):
|
|||||||
texture.interpolation, texture.extension = "Linear", "EXTEND"
|
texture.interpolation, texture.extension = "Linear", "EXTEND"
|
||||||
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
||||||
links.new(texture_input.outputs["Depth Image"], texture.inputs["Image"])
|
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"})
|
threshold_input = _group_input(nodes, {"Validity Threshold"})
|
||||||
channels = nodes.new("FunctionNodeSeparateColor")
|
channels = nodes.new("FunctionNodeSeparateColor")
|
||||||
channels.mode = "RGB"
|
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(cull_controls.outputs["Valid Only"], cull.inputs["Switch"])
|
||||||
links.new(delete.outputs["Geometry"], cull.inputs["True"])
|
links.new(delete.outputs["Geometry"], cull.inputs["True"])
|
||||||
links.new(plane.outputs["Geometry"], cull.inputs["False"])
|
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(
|
geometry = expand_smooth(
|
||||||
group, clean, iterations_name="Smooth", weight_name="Smooth Weight",
|
group, clean, iterations_name="Smooth", weight_name="Smooth Weight",
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user