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
|
||||
|
||||
# 构建并验证 Blender 节点资产
|
||||
uv run build_node
|
||||
uv run build_nodes
|
||||
|
||||
# 准备、校验并同步 ONNX 模型到 R2
|
||||
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 --prepare` | 为本机同步扩展 wheel 与 Manifest |
|
||||
| `uv run build_node` | 构建、验证节点资产并运行测试 |
|
||||
| `uv run build_nodes` | 构建、验证节点资产并运行测试 |
|
||||
| `uv run sync_models` | 准备、校验并上传模型声明的文件 |
|
||||
| `uv run docs -dev` | 本地预览文档 |
|
||||
| `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`
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
保存后,`check.py` 在独立 Blender 进程中重新加载资产并做基本几何求值。接口、默认值和详细几何行为由 `tests/` 检查。
|
||||
|
||||
```bash
|
||||
uv run build_node --blender "/path/to/blender"
|
||||
uv run build_node --skip-tests
|
||||
uv run build_nodes --blender "/path/to/blender"
|
||||
uv run build_nodes --skip-tests
|
||||
```
|
||||
|
||||
脚本按 `--blender`、环境变量 `BLENDER_BIN`、项目 `.env`、`PATH` 的顺序查找 Blender。无法自动找到时,在项目根目录的 `.env` 中配置:
|
||||
|
||||
@@ -27,7 +27,7 @@ dev = [
|
||||
|
||||
[project.scripts]
|
||||
pack = "tools.pack:main"
|
||||
build_node = "tools.nodes.cli:main"
|
||||
build_nodes = "tools.nodes.cli:main"
|
||||
sync_models = "tools.models.cli:main"
|
||||
docs = "tools.docs:main"
|
||||
|
||||
@@ -37,3 +37,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["tools"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
# Keep native asserts: bpy 4.5 can crash at shutdown after assertion rewriting.
|
||||
addopts = "--assert=plain"
|
||||
|
||||
Binary file not shown.
0
tests/__init__.py
Normal file
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.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import mock_bpy_imports
|
||||
from tests.support.blender import mock_bpy_imports
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
def load_common_modules():
|
||||
fake_bpy = ModuleType("bpy")
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
@@ -7,7 +5,6 @@ import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common import image as images
|
||||
from anyimage.operators.convert_to_plane.object import create_color_image
|
||||
@@ -1,12 +1,7 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import numpy as np
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.selection import SelectionMask
|
||||
from anyimage.common.depth import (
|
||||
@@ -1,12 +1,9 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common import image as images
|
||||
|
||||
@@ -2,8 +2,7 @@ import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from common_test_support import load_common_modules
|
||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
||||
from tests.anyimage.common.support import load_common_modules
|
||||
|
||||
class ImageDataTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -3,23 +3,11 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class ImageEditToolTest(BlenderTestCase):
|
||||
def test_image_edit_tools_share_the_plain_pixel_edit_result_creator(self):
|
||||
self.assertIs(
|
||||
self.edit_mask.create_image_edit_result,
|
||||
self.image_data.create_image_edit_result,
|
||||
)
|
||||
self.assertIs(
|
||||
self.rectify.create_image_edit_result,
|
||||
self.image_data.create_image_edit_result,
|
||||
)
|
||||
self.assertFalse(hasattr(self.image_data, "create_pixel_result_image"))
|
||||
|
||||
|
||||
def test_packed_image_export_does_not_use_scene_render_transform(self):
|
||||
@@ -222,12 +210,3 @@ class ImageEditToolTest(BlenderTestCase):
|
||||
"Select an active Image Empty",
|
||||
):
|
||||
self.image_interaction.active_image_edit_source(context)
|
||||
|
||||
|
||||
def test_blender_image_exports_do_not_use_scene_render_color_management(self):
|
||||
source_root = PROJECT_ROOT / "src" / "anyimage"
|
||||
offenders = []
|
||||
for path in source_root.rglob("*.py"):
|
||||
if ".save_render(" in path.read_text(encoding="utf-8"):
|
||||
offenders.append(path.relative_to(source_root))
|
||||
self.assertEqual(offenders, [])
|
||||
@@ -1,21 +1,14 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import material as material_data
|
||||
from anyimage.operators.convert_to_plane import object as plane_object
|
||||
from anyimage.operators.cutout_tool import object as cutout_object
|
||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
||||
from image_object_test_support import _create_cutout_shape, _group_node, _inputs
|
||||
from tests.support.image_objects import _create_cutout_shape, _group_node, _inputs
|
||||
|
||||
|
||||
def test_image_layer_switches_between_tangent_and_object_space_normal():
|
||||
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
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from common_test_support import load_common_modules
|
||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
||||
from tests.anyimage.common.support import load_common_modules
|
||||
|
||||
class MaterialSettingsTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
@@ -7,7 +5,6 @@ import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage import preferences
|
||||
from anyimage.common import image as images
|
||||
@@ -1,8 +1,6 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from common_test_support import load_common_modules
|
||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
||||
from tests.anyimage.common.support import load_common_modules
|
||||
|
||||
class ModifierInputsTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -1,7 +1,6 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from common_test_support import load_common_modules
|
||||
PROJECT_ROOT=Path(__file__).resolve().parents[1]
|
||||
from tests.anyimage.common.support import load_common_modules
|
||||
|
||||
class NodeLoadingTest(unittest.TestCase):
|
||||
@classmethod
|
||||
@@ -1,11 +1,7 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common import selection
|
||||
|
||||
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 types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class AiImageOperatorsTest(BlenderTestCase):
|
||||
@@ -3,7 +3,7 @@ import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class AiSetupTest(BlenderTestCase):
|
||||
@@ -1,14 +1,6 @@
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
from anyimage.operators.cutout_tool.geometry import poisson_balloon_profile
|
||||
@@ -1,11 +1,9 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common import selection, viewport
|
||||
from anyimage.operators import mask_tool as mask
|
||||
@@ -1,16 +1,12 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from blender_test_support import mock_bpy_imports
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
||||
from tests.support.blender import mock_bpy_imports
|
||||
|
||||
|
||||
class ClipboardImageTest(unittest.TestCase):
|
||||
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 pytest
|
||||
from scipy.sparse import coo_matrix
|
||||
from scipy.sparse.csgraph import connected_components
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
from anyimage.operators.cutout_tool import geometry as g
|
||||
from anyimage.operators.cutout_tool import fine_mesh as fine
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
|
||||
import builtins
|
||||
import unittest
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.selection import SelectionPath, rasterize_selection_path
|
||||
from anyimage.operators.cutout_tool import geometry as cutout_geometry
|
||||
@@ -1,10 +1,6 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common.selection import SelectionPath
|
||||
from anyimage.common import viewport
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
@@ -7,9 +5,6 @@ import numpy as np
|
||||
import pytest
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from anyimage.common import depth as depth_data
|
||||
from anyimage.common import image as image_data
|
||||
@@ -24,7 +19,7 @@ from anyimage.server.geometry.depth_texture import (
|
||||
write_depth_texture,
|
||||
)
|
||||
from anyimage.server.models.geometry import GeometryFrame
|
||||
from image_object_test_support import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node, _inputs
|
||||
from tests.support.image_objects import _add_cutout_modifier, _create_cutout_shape, _depth_image, _depth_metadata, _depth_surface_step_object, _evaluated_positions, _evaluated_topology, _group_node, _inputs
|
||||
|
||||
|
||||
def test_object_normal_image_follows_cutout_inward_axis():
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class CutoutOperatorsTest(BlenderTestCase):
|
||||
@@ -1,10 +1,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common.image import premultiplied_rgba
|
||||
from anyimage.operators import frame_tool as frame
|
||||
@@ -1,9 +1,6 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common.selection import clip_polygon_halfplanes
|
||||
from anyimage.operators import frame_tool as frame
|
||||
@@ -1,6 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class FrameToolTest(BlenderTestCase):
|
||||
@@ -1,14 +1,8 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Matrix
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common.selection import SelectionMask
|
||||
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class MaskToolTest(BlenderTestCase):
|
||||
@@ -1,11 +1,8 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common import image, viewport
|
||||
from anyimage.operators import mask_tool as mask
|
||||
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
|
||||
import bpy
|
||||
import numpy as np
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import object as object_data
|
||||
from anyimage.operators.clipboard_image import actions as clipboard_actions
|
||||
from anyimage.operators.convert_to_plane import object as plane_object
|
||||
from anyimage.operators.cutout_tool import shape as cutout_shape
|
||||
from image_object_test_support import _depth_image, _depth_metadata, _evaluated_positions, _inputs
|
||||
from tests.support.image_objects import _depth_image, _depth_metadata, _evaluated_positions, _inputs
|
||||
from tools.nodes.groups.image_plane import build_image_plane_group
|
||||
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
|
||||
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class PlaneOperatorsTest(BlenderTestCase):
|
||||
@@ -1,5 +1,5 @@
|
||||
from types import SimpleNamespace
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class RectifyToolTest(BlenderTestCase):
|
||||
@@ -1,12 +1,9 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import bpy
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
from anyimage.operators.upscale import UpscaleImage
|
||||
from anyimage.common.image import image_pixels
|
||||
|
||||
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 sys
|
||||
import tempfile
|
||||
@@ -6,8 +7,6 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def load_server_module():
|
||||
server_directory = PROJECT_ROOT / "src" / "anyimage" / "server"
|
||||
specification = importlib.util.spec_from_file_location(
|
||||
@@ -1,11 +1,9 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_server_module(name):
|
||||
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
||||
sys.path.insert(0, str(addon_directory))
|
||||
@@ -1,12 +1,9 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src" / "anyimage"))
|
||||
|
||||
from server.geometry import prediction_artifacts as artifacts
|
||||
from server.geometry.edge_depth import extend_edge_depth
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from server_runtime_test_support import ServerTestCase
|
||||
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||
|
||||
class GeometryArtifactsTest(ServerTestCase):
|
||||
def test_depth_alpha_multiplies_coverage_without_changing_geometry(self):
|
||||
@@ -1,11 +1,8 @@
|
||||
|
||||
|
||||
|
||||
import threading
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from server_runtime_test_support import ServerTestCase
|
||||
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||
|
||||
class JobServerTest(ServerTestCase):
|
||||
def test_dedicated_server_runs_only_one_job_at_a_time(self):
|
||||
@@ -171,4 +168,3 @@ class JobServerTest(ServerTestCase):
|
||||
"generate-cutout-artifacts",
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from server_test_support import load_server_module, _make_fake_av
|
||||
from tests.anyimage.server.support import load_server_module, _make_fake_av
|
||||
|
||||
|
||||
class MediaInputTest(unittest.TestCase):
|
||||
@@ -1,22 +1,11 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
SOURCE_DIR = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "server"
|
||||
/ "models"
|
||||
)
|
||||
sys.path.insert(0, str(SOURCE_DIR))
|
||||
|
||||
import onnx_ben2
|
||||
from server.models import onnx_ben2
|
||||
|
||||
|
||||
class FakeSession:
|
||||
@@ -1,9 +1,6 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib.util
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_module(name, relative_path):
|
||||
@@ -1,3 +1,4 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
@@ -7,9 +8,6 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_download_module():
|
||||
addon_directory = PROJECT_ROOT / "src" / "anyimage"
|
||||
sys.path.insert(0, str(addon_directory))
|
||||
@@ -5,7 +5,7 @@ import unittest
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from server_test_support import load_server_module
|
||||
from tests.anyimage.server.support import load_server_module
|
||||
|
||||
|
||||
class ModelDownloadJobsTest(unittest.TestCase):
|
||||
@@ -1,14 +1,6 @@
|
||||
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.server.models import moge2 as moge2_model
|
||||
from anyimage.server.models.geometry import GeometryFrame
|
||||
@@ -6,7 +6,7 @@ from unittest.mock import patch
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from server_test_support import load_server_module, FakeModels
|
||||
from tests.anyimage.server.support import load_server_module, FakeModels
|
||||
|
||||
|
||||
class ModelInferenceTest(unittest.TestCase):
|
||||
@@ -1,11 +1,9 @@
|
||||
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from server_runtime_test_support import ServerTestCase
|
||||
from tests.anyimage.server.runtime_support import ServerTestCase
|
||||
|
||||
class ModelManagerTest(ServerTestCase):
|
||||
def test_required_model_download_skips_ready_model(self):
|
||||
@@ -182,5 +180,3 @@ class ModelManagerTest(ServerTestCase):
|
||||
lambda: cache.get_upscale("upscale", "cuda"),
|
||||
"release_upscale",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib.util
|
||||
import sys
|
||||
import unittest
|
||||
@@ -7,9 +8,6 @@ from unittest.mock import patch
|
||||
import numpy as np
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_module():
|
||||
anyimage = ModuleType("anyimage")
|
||||
anyimage.__path__ = [str(PROJECT_ROOT / "src" / "anyimage")]
|
||||
@@ -1,20 +1,10 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
SOURCE_DIR = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "server"
|
||||
/ "models"
|
||||
)
|
||||
sys.path.insert(0, str(SOURCE_DIR))
|
||||
|
||||
import onnx_runtime
|
||||
from server.models import onnx_runtime
|
||||
|
||||
|
||||
class FakeSession:
|
||||
@@ -211,6 +201,5 @@ class OnnxRuntimeTest(unittest.TestCase):
|
||||
self.assertEqual(providers, ["CPUExecutionProvider"])
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,13 +1,10 @@
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src/anyimage"))
|
||||
from server.models import onnx_pisa_sr as pisa
|
||||
from server.model_manager import ModelManager, ModelSessionCache
|
||||
from server import model_catalog
|
||||
@@ -1,22 +1,11 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
|
||||
SOURCE_DIR = (
|
||||
Path(__file__).resolve().parents[1]
|
||||
/ "src"
|
||||
/ "anyimage"
|
||||
/ "server"
|
||||
/ "models"
|
||||
)
|
||||
sys.path.insert(0, str(SOURCE_DIR))
|
||||
|
||||
import onnx_upscale
|
||||
from server.models import onnx_upscale
|
||||
|
||||
|
||||
class FakeSession:
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
from server_test_support import load_server_module, FakeJobContext
|
||||
from tests.anyimage.server.support import load_server_module, FakeJobContext
|
||||
|
||||
|
||||
class ServerJobsTest(unittest.TestCase):
|
||||
@@ -2,7 +2,7 @@ import importlib
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class AddonRegistrationTest(BlenderTestCase):
|
||||
@@ -1,6 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class ImageMenuTest(BlenderTestCase):
|
||||
@@ -94,8 +94,6 @@ class ImageMenuTest(BlenderTestCase):
|
||||
self.assertNotIn(("anyimage.setup_ai_environment", True), calls)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_image_operators_stay_clickable_until_server_is_busy(self):
|
||||
context = SimpleNamespace(
|
||||
object=SimpleNamespace(data=SimpleNamespace()),
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class JobOperatorTest(BlenderTestCase):
|
||||
@@ -2,7 +2,7 @@ import importlib
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class PreferencesTest(BlenderTestCase):
|
||||
@@ -1,7 +1,7 @@
|
||||
import importlib
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
from blender_test_support import BlenderTestCase
|
||||
from tests.support.blender import BlenderTestCase
|
||||
|
||||
|
||||
class ServerPanelTest(BlenderTestCase):
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Expose project source packages to the test suite."""
|
||||
"""Expose the extension and standalone server packages to the test suite."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
sys.path[:0] = [str(PROJECT_ROOT / "src"), str(PROJECT_ROOT / "src/anyimage")]
|
||||
|
||||
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 builtins
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src"
|
||||
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
|
||||
from node_asset_test_support import CUTOUT_BUILDERS
|
||||
from tests.support.nodes import CUTOUT_BUILDERS
|
||||
|
||||
|
||||
def surface(*, step=6.0, size=1.0, resolution=2, wrapper=False):
|
||||
@@ -1,14 +1,7 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
|
||||
SOURCE_ROOT = Path(__file__).parents[1] / "src"
|
||||
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
from anyimage.common import image as image_data
|
||||
from anyimage.common import object as object_data
|
||||
from anyimage.common.selection import SelectionMask
|
||||
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 sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_sync_models():
|
||||
path = PROJECT_ROOT / "tools" / "models" / "cli.py"
|
||||
spec = importlib.util.spec_from_file_location("sync_models", path)
|
||||
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_depth_plane import build_image_depth_plane_group
|
||||
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||
from node_asset_test_support import CUTOUT_BUILDERS
|
||||
from tests.support.nodes import CUTOUT_BUILDERS
|
||||
from tools.nodes.arrangement.arrange import BRANCH_GAP, FIELD_HEIGHT, ROUTE_RUN, arrange_nodes
|
||||
from tools.nodes.arrangement.flow import geometry_paths, origin_socket, peer_links
|
||||
from tools.nodes.arrangement.dimensions import RULES, estimate_node_dimensions
|
||||
@@ -1,10 +1,9 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT_PATH = PROJECT_ROOT / "tools" / "nodes" / "cli.py"
|
||||
|
||||
|
||||
@@ -78,7 +77,7 @@ def test_launcher_runs_asset_build_and_validations_before_tests(monkeypatch, tmp
|
||||
calls = []
|
||||
monkeypatch.setattr(launcher, "blender_path", lambda _: executable)
|
||||
monkeypatch.setattr(launcher, "run", calls.append)
|
||||
monkeypatch.setattr("sys.argv", ["build_node"])
|
||||
monkeypatch.setattr("sys.argv", ["build_nodes"])
|
||||
|
||||
launcher.main()
|
||||
|
||||
@@ -3,17 +3,20 @@ from collections import Counter
|
||||
import bpy
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from tests.support.nodes import CUTOUT_BUILDERS
|
||||
|
||||
from anyimage.operators.cutout_tool.object import load_cutout_node_group
|
||||
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
from node_asset_test_support import validate_node_structure
|
||||
from tests.support.nodes import validate_node_structure
|
||||
|
||||
|
||||
def test_saved_cutout_defaults():
|
||||
@pytest.mark.parametrize("source", ["definition", "asset"])
|
||||
def test_cutout_interfaces_and_thickness_modes(source):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
groups = {shape: load_cutout_node_group(shape) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
|
||||
groups = {shape: (CUTOUT_BUILDERS[shape]() if source == "definition" else load_cutout_node_group(shape)) for shape in ("FLAT", "SOLID", "DEPTH_SOLID", "DEPTH_BALLOON")}
|
||||
assert groups["FLAT"] == groups["SOLID"]
|
||||
assert len(set(groups.values())) == 3
|
||||
for group in set(groups.values()):
|
||||
@@ -80,4 +83,3 @@ def test_saved_cutout_defaults():
|
||||
assert set(uses.values()) == {2}
|
||||
finally:
|
||||
evaluated.to_mesh_clear()
|
||||
print("Validated three Cutout assets, four presets, both thickness modes and closed defaults")
|
||||
@@ -1,70 +1,17 @@
|
||||
"""Contracts for smooth depth volume, editable presets and normal masks."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
from node_asset_test_support import CUTOUT_BUILDERS
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_fields
|
||||
from tools.nodes.groups.image_depth_cutout import build_volume_rounding
|
||||
from anyimage.operators.cutout_tool.object import load_cutout_node_group
|
||||
from anyimage.operators.cutout_tool.operators import normal_mode_for_shape
|
||||
from anyimage.operators.cutout_tool.geometry import refine_balloon_triangles, poisson_balloon_profile
|
||||
from anyimage.common.object import modifier_input_identifier, set_modifier_input
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
def test_depth_presets_always_generate_the_correct_normal_space(enabled):
|
||||
assert normal_mode_for_shape("DEPTH_SOLID", enabled) == "OBJECT"
|
||||
assert normal_mode_for_shape("DEPTH_BALLOON", enabled) == "TANGENT"
|
||||
for shape in ("FLAT", "SOLID"):
|
||||
assert normal_mode_for_shape(shape, enabled) == ("TANGENT" if enabled else "NONE")
|
||||
|
||||
|
||||
def test_new_cutout_does_not_reuse_a_saved_wrapper():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
saved = bpy.data.node_groups.new("O Image Cutout", "GeometryNodeTree")
|
||||
saved.interface.new_socket(name="Shape", in_out="INPUT", socket_type="NodeSocketMenu")
|
||||
saved.use_fake_user = True
|
||||
current = load_cutout_node_group("SOLID")
|
||||
assert current != saved and current.get("anyimage_asset_revision") == 11
|
||||
assert saved in bpy.data.node_groups.values()
|
||||
assert current == load_cutout_node_group("FLAT")
|
||||
assert any(item.name == "Shape" for item in saved.interface.items_tree)
|
||||
|
||||
|
||||
def test_refined_boundary_ears_have_interior_balloon_samples():
|
||||
points = np.array([(0, 0), (1, 0), (1, 1), (0, 1)], dtype=float)
|
||||
points, faces, boundary = refine_balloon_triangles(points, [(0, 1, 2), (0, 2, 3)], np.ones(4, bool))
|
||||
profile = poisson_balloon_profile(points, faces, boundary)
|
||||
assert np.all(profile[boundary] == 0)
|
||||
assert all(any(profile[index] > 0 for index in face) for face in faces)
|
||||
|
||||
|
||||
def test_depth_rounding_options_are_consolidated():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = CUTOUT_BUILDERS["DEPTH_SOLID"]()
|
||||
inputs = {s.name: s for s in group.interface.items_tree if s.item_type == "SOCKET" and s.in_out == "INPUT"}
|
||||
for name, default in (("Edge Round", 0.2), ("Front Inflation", 1), ("Normal Smooth", 50)):
|
||||
assert inputs[name].default_value == pytest.approx(default)
|
||||
assert inputs[name].structure_type == "SINGLE"
|
||||
assert inputs["Edge Round"].parent.name == "Options"
|
||||
names = list(inputs)
|
||||
assert names[names.index("Front Inflation") - 1] == "Edge Round"
|
||||
assert [s.name for s in inputs.values() if s.parent and s.parent.name == "Options"] == [
|
||||
"Cleanup Threshold", "Split Threshold", "Reference Depth", "Edge Round", "Front Inflation", "Normal Smooth", "Smooth Weight",
|
||||
]
|
||||
assert not set(inputs) & {"Edge Round Range", "Back Rim Range", "Normal Influence", "Edge Round Amount", "Back Rim Fullness"}
|
||||
assert all(n.inputs["Name"].default_value != "unused" for n in group.nodes if n.bl_idname == "GeometryNodeRemoveAttribute")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("iterations", [0, 50])
|
||||
def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
|
||||
from scipy.spatial import cKDTree
|
||||
from depth_surface_test_support import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
obj, set_value = surface(step=0, resolution=4)
|
||||
image = bpy.data.images["Camera"]
|
||||
pixels = np.array(image.pixels[:], dtype=np.float32).reshape(32, 64, 4)
|
||||
@@ -83,7 +30,7 @@ def test_uniform_thickness_follows_the_tilted_front_normal(iterations):
|
||||
|
||||
def test_balloon_shell_converges_to_front_with_thickness():
|
||||
from scipy.spatial import cKDTree
|
||||
from depth_surface_test_support import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
@@ -196,7 +143,7 @@ def test_smooth_back_rejects_front_noise_and_preserves_center_depth():
|
||||
|
||||
|
||||
def test_depth_cutout_normal_map_only_affects_front_faces():
|
||||
from depth_surface_test_support import surface
|
||||
from tests.support.depth_surface import surface
|
||||
obj, set_value = surface()
|
||||
set_value("Thickness", 0.3)
|
||||
bpy.context.view_layer.update()
|
||||
@@ -215,7 +162,7 @@ def test_depth_cutout_normal_map_only_affects_front_faces():
|
||||
@pytest.mark.parametrize("structure", [0, 1, 4])
|
||||
@pytest.mark.parametrize("thickness", [0.0, 0.02, 0.1])
|
||||
def test_uniform_front_inflation_scales_with_distance(thickness, structure):
|
||||
from depth_surface_test_support import surface, evaluated, assert_closed
|
||||
from tests.support.depth_surface import surface, evaluated, assert_closed
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
@@ -243,7 +190,7 @@ def test_uniform_front_inflation_scales_with_distance(thickness, structure):
|
||||
|
||||
|
||||
def test_normal_smooth_controls_front_and_rear_directions():
|
||||
from depth_surface_test_support import surface, evaluated
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
obj, set_value = surface(step=1, resolution=10)
|
||||
set_value("Front Inflation", 1.0)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
@@ -272,7 +219,7 @@ def test_normal_smooth_controls_front_and_rear_directions():
|
||||
|
||||
|
||||
def test_balloon_suppression_is_stronger_without_a_startup_threshold():
|
||||
from depth_surface_test_support import surface, evaluated
|
||||
from tests.support.depth_surface import surface, evaluated
|
||||
obj, set_value = surface(step=0, resolution=10)
|
||||
xy = np.array([v.co[:2] for v in obj.data.vertices])
|
||||
profile = 0.3 * np.sin(np.pi * xy[:, 0] / 2) * np.sin(np.pi * xy[:, 1])
|
||||
@@ -1,90 +1,15 @@
|
||||
"""Evaluate complete-image projection and rectangular surface topology."""
|
||||
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
from tools.nodes.groups.image_plane import build_image_plane_group
|
||||
from tools.nodes.groups.image_depth_plane import build_image_depth_plane_group
|
||||
from tools.nodes.groups.image_relief_plane import build_image_relief_plane_group
|
||||
from node_asset_test_support import validate_node_structure
|
||||
from tests.support.planes import surface, relief, evaluated
|
||||
from tests.support.nodes import validate_node_structure
|
||||
from tools.nodes.arrangement.flow import origin_socket
|
||||
|
||||
|
||||
@pytest.fixture(params=("definition", "asset"))
|
||||
def surface(request):
|
||||
return create_surface(request.param, "DEPTH")
|
||||
|
||||
|
||||
@pytest.fixture(params=("definition", "asset"))
|
||||
def relief(request):
|
||||
return create_surface(request.param, "RELIEF")
|
||||
|
||||
|
||||
def create_surface(source, plane_type):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group_name = f"O Image {plane_type.title()} Plane"
|
||||
if source == "asset":
|
||||
with bpy.data.libraries.load(str(ROOT / "src/anyimage/assets/O_AnyImage.blend")) as (library, target):
|
||||
target.node_groups = [group_name]
|
||||
group = target.node_groups[0]
|
||||
plane = group.nodes["Group"].node_tree
|
||||
else:
|
||||
plane = build_image_plane_group()
|
||||
builder = build_image_depth_plane_group if plane_type == "DEPTH" else build_image_relief_plane_group
|
||||
group = builder(plane)
|
||||
mesh = bpy.data.meshes.new("Rectangle")
|
||||
mesh.from_pydata([(-2, -1, 0), (2, -1, 0), (2, 1, 0), (-2, 1, 0)], [], [(0, 1, 2, 3)])
|
||||
material = bpy.data.materials.new("Image")
|
||||
mesh.materials.append(material)
|
||||
obj = bpy.data.objects.new("Surface", mesh)
|
||||
bpy.context.collection.objects.link(obj)
|
||||
modifier = obj.modifiers.new("Surface", "NODES")
|
||||
modifier.node_group = group
|
||||
image = bpy.data.images.new("Camera", width=16, height=8, float_buffer=True)
|
||||
image.colorspace_settings.name = "Non-Color"
|
||||
image.alpha_mode = "CHANNEL_PACKED"
|
||||
yy, xx = np.mgrid[:8, :16]
|
||||
# Camera principal point is deliberately off center; Y points down.
|
||||
pixels = np.ones((8, 16, 4), dtype=np.float32)
|
||||
pixels[..., 0] = ((xx + 0.5) - 5) / 4
|
||||
pixels[..., 1] = (3 - (yy + 0.5)) / 4
|
||||
pixels[..., 2] = 2 + (xx + 0.5) / 16
|
||||
pixels[..., 3] = 1
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
inputs = {item.name: item for item in group.interface.items_tree
|
||||
if item.item_type == "SOCKET" and item.in_out == "INPUT"}
|
||||
def set_value(name, value):
|
||||
modifier[inputs[name].identifier] = value
|
||||
obj.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
for name, value in (("Subdivide", 2), ("Depth Image", image),
|
||||
("Uniform Scale", 0.5), ("Reference Depth", 1.2),
|
||||
("Depth Scale", 0.0), ("Thickness", 0.0)):
|
||||
set_value(name, value)
|
||||
return obj, set_value, plane, inputs, image
|
||||
|
||||
|
||||
def evaluated(obj):
|
||||
result = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
|
||||
mesh = result.to_mesh()
|
||||
try:
|
||||
return (
|
||||
np.array([v.co[:] for v in mesh.vertices]),
|
||||
[tuple(face.vertices) for face in mesh.polygons],
|
||||
np.array([value.uv[:] for value in mesh.uv_layers["UVMap"].data]) if mesh.uv_layers else np.empty((0, 2)),
|
||||
[material.name if material else None for material in mesh.materials],
|
||||
[face.material_index for face in mesh.polygons],
|
||||
[attr.name for attr in mesh.attributes],
|
||||
)
|
||||
finally:
|
||||
result.to_mesh_clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subdivide", [0, 1, 3])
|
||||
def test_zero_depth_matches_plane_geometry_and_uv(surface, subdivide):
|
||||
obj, set_value, plane, inputs, _ = surface
|
||||
@@ -148,6 +73,41 @@ def test_shell_keeps_front_and_closes_rectangular_boundary(surface, thickness):
|
||||
assert not any(name.startswith("o_depth_") for name in attributes)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("valid_only", [False, True])
|
||||
def test_depth_split_preserves_faces_and_closes_each_shell(surface, valid_only):
|
||||
from tests.support.depth_surface import component_count
|
||||
|
||||
obj, set_value, _, _, image = surface
|
||||
pixels = np.asarray(image.pixels[:], dtype=np.float32).reshape(8, 16, 4)
|
||||
pixels[:, 8:, 2] += 10
|
||||
pixels[:2, :, 3] = 0
|
||||
image.pixels.foreach_set(pixels.ravel())
|
||||
image.update()
|
||||
set_value("Depth Scale", 1.0)
|
||||
set_value("Valid Only", valid_only)
|
||||
base = evaluated(obj)
|
||||
set_value("Split Threshold", 1.0)
|
||||
points, faces, uv, materials, indices, attributes = evaluated(obj)
|
||||
assert len(points) > len(base[0])
|
||||
assert len(faces) == len(base[1])
|
||||
assert component_count(faces) > component_count(base[1])
|
||||
np.testing.assert_allclose(uv, base[2])
|
||||
assert (materials, indices) == base[3:5]
|
||||
assert not any(name.startswith("o_depth_") for name in attributes)
|
||||
set_value("Thickness", 0.2)
|
||||
shell_points, shell_faces, shell_uv, *_ = evaluated(obj)
|
||||
assert len(shell_points) == 2 * len(points)
|
||||
edges = Counter(tuple(sorted((a, b))) for face in shell_faces
|
||||
for a, b in zip(face, (*face[1:], face[0])))
|
||||
assert set(edges.values()) == {2}
|
||||
assert np.isfinite(shell_points).all() and np.isfinite(shell_uv).all()
|
||||
set_value("Thickness", 0.0)
|
||||
set_value("Split Threshold", 0.0)
|
||||
restored = evaluated(obj)
|
||||
np.testing.assert_allclose(restored[0], base[0])
|
||||
assert restored[1] == base[1]
|
||||
|
||||
|
||||
def test_depth_jump_keeps_rectangular_faces(surface):
|
||||
obj, set_value, _, _, image = surface
|
||||
base = evaluated(obj)
|
||||
@@ -229,7 +189,12 @@ def test_curved_shell_smooths_normals_without_shrinking_thickness(surface, norma
|
||||
def test_surface_interface_and_node_structure(surface):
|
||||
obj, _, _, inputs, _ = surface
|
||||
assert list(inputs) == ["Geometry", "Subdivide", "Thickness", "Depth Scale", "Smooth", "Valid Only",
|
||||
"Reference Depth", "Normal Smooth", "Smooth Weight", "Validity Threshold", "Uniform Scale", "Depth Image"]
|
||||
"Reference Depth", "Normal Smooth", "Smooth Weight", "Validity Threshold", "Split Threshold", "Uniform Scale", "Depth Image"]
|
||||
assert inputs["Split Threshold"].default_value == 0
|
||||
assert inputs["Split Threshold"].min_value == 0
|
||||
assert inputs["Split Threshold"].max_value == 1
|
||||
assert inputs["Split Threshold"].subtype == "FACTOR"
|
||||
assert inputs["Split Threshold"].parent.name == "Options"
|
||||
assert inputs["Validity Threshold"].default_value == pytest.approx(0.9)
|
||||
assert inputs["Validity Threshold"].parent.name == "Options"
|
||||
assert inputs["Validity Threshold"].subtype == "FACTOR"
|
||||
@@ -265,71 +230,6 @@ def test_relief_interface_and_node_structure(relief):
|
||||
validate_node_structure(obj.modifiers[0].node_group)
|
||||
|
||||
|
||||
def test_conversion_preserves_offset_world_placement_and_initializes_inputs(surface, monkeypatch):
|
||||
from anyimage.operators.convert_to_plane import object as conversion
|
||||
from anyimage.common.image import image_empty_bounds
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
obj, _, _, inputs, image = surface
|
||||
group = obj.modifiers[0].node_group
|
||||
monkeypatch.setattr(conversion, "load_node_group", lambda name: group)
|
||||
source = bpy.data.objects.new("Offset Image", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
source.empty_display_size = 4
|
||||
source.empty_image_offset = (-0.2, -0.8)
|
||||
source.matrix_world = Matrix.Translation((3, -2, 5)) @ Matrix.Rotation(0.7, 4, "Y") @ Matrix.Diagonal((2, 3, 1, 1))
|
||||
bpy.context.collection.objects.link(source)
|
||||
bounds = image_empty_bounds(source)
|
||||
matrix = source.matrix_world.copy()
|
||||
material = bpy.data.materials.new("Conversion")
|
||||
metadata = {"image_size": (16, 8), "reference_depth": 2.0,
|
||||
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
|
||||
result = conversion.create_depth_plane_object(
|
||||
bpy.context, source, 2, material, image, metadata, "DEPTH")
|
||||
assert result.name == "Offset Image"
|
||||
assert result["anyimage_mesh_shape"] == "DEPTH_PLANE"
|
||||
assert bpy.context.view_layer.objects.active == result
|
||||
expected = sorted(tuple(matrix @ Vector((x, y, 0)))
|
||||
for x in bounds[:2] for y in bounds[2:])
|
||||
actual = sorted(tuple(result.matrix_world @ v.co) for v in result.data.vertices)
|
||||
np.testing.assert_allclose(actual, expected, atol=1e-6)
|
||||
center = matrix @ Vector(((bounds[0] + bounds[1]) / 2, (bounds[2] + bounds[3]) / 2, 0))
|
||||
np.testing.assert_allclose(result.matrix_world.translation, center, atol=1e-6)
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier[inputs["Thickness"].identifier] == 0
|
||||
assert modifier[inputs["Depth Image"].identifier] == image
|
||||
assert modifier[inputs["Valid Only"].identifier] is True
|
||||
assert modifier[inputs["Reference Depth"].identifier] == pytest.approx(
|
||||
2 * modifier[inputs["Uniform Scale"].identifier])
|
||||
assert result.data.materials[0] == material
|
||||
|
||||
|
||||
def test_failed_modifier_initialization_removes_partial_object(surface, monkeypatch):
|
||||
from anyimage.operators.convert_to_plane import object as conversion
|
||||
|
||||
obj, _, _, _, image = surface
|
||||
monkeypatch.setattr(conversion, "load_node_group", lambda name: obj.modifiers[0].node_group)
|
||||
source = bpy.data.objects.new("Source", None)
|
||||
source.empty_display_type = "IMAGE"
|
||||
source.data = image
|
||||
bpy.context.collection.objects.link(source)
|
||||
material = bpy.data.materials.new("Unused")
|
||||
before_objects, before_meshes = set(bpy.data.objects), set(bpy.data.meshes)
|
||||
|
||||
def fail(*args):
|
||||
raise RuntimeError("input failed")
|
||||
|
||||
monkeypatch.setattr(conversion, "set_modifier_input", fail)
|
||||
metadata = {"image_size": (16, 8), "reference_depth": 2,
|
||||
"intrinsics": ((4, 0, 5), (0, 4, 3), (0, 0, 1))}
|
||||
with pytest.raises(RuntimeError, match="input failed"):
|
||||
conversion.create_depth_plane_object(bpy.context, source, 2, material, image, metadata, "DEPTH")
|
||||
assert set(bpy.data.objects) == before_objects
|
||||
assert set(bpy.data.meshes) == before_meshes
|
||||
assert material.users == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("threshold", [0.0, 0.4, 0.9, 1.0])
|
||||
@pytest.mark.parametrize("pattern", ["valid", "invalid", "partial", "half", "stripe"])
|
||||
def test_valid_only_recomputes_per_point_and_preserves_boundary_faces(surface, pattern, threshold):
|
||||
@@ -400,10 +300,9 @@ def test_validity_graph_matches_reference_domains_and_connections(surface):
|
||||
assert alpha.name == "Alpha" and alpha.node.bl_idname == "FunctionNodeSeparateColor"
|
||||
color = origin_socket(alpha.node.inputs["Color"].links[0].from_socket).node
|
||||
assert origin_socket(color.inputs["Image"].links[0].from_socket).name == "Depth Image"
|
||||
assert len([node for node in color.id_data.nodes if node.bl_idname == "GeometryNodeImageTexture"]) == 1
|
||||
consumers = [link for link in group.links if link.to_node.bl_idname != "NodeReroute"
|
||||
and origin_socket(link.from_socket) == color.outputs["Color"]]
|
||||
assert len(consumers) == 2
|
||||
assert any(link.to_node == alpha.node for link in consumers)
|
||||
assert origin_socket(delete.inputs["Geometry"].links[0].from_socket).node.node_tree.name == "O Image Plane"
|
||||
|
||||
|
||||
@@ -430,3 +329,19 @@ def test_depth_smooth_changes_positions_and_preserves_topology(surface, thicknes
|
||||
set_value("Smooth Weight", 1.0)
|
||||
set_value("Smooth", 0)
|
||||
np.testing.assert_allclose(evaluated(obj)[0], base[0], atol=1e-6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["DEPTH", "RELIEF"])
|
||||
@pytest.mark.parametrize("source", ["definition", "asset"])
|
||||
def test_plane_interface_panels(source, kind):
|
||||
from tests.support.planes import create_surface
|
||||
obj, _, _, _, _ = create_surface(source, kind)
|
||||
node_group = obj.modifiers[0].node_group
|
||||
panels = [item for item in node_group.interface.items_tree if item.item_type == "PANEL"]
|
||||
assert [item.name for item in panels] == ["Options", "Data"]
|
||||
controls = ["Reference Depth"]
|
||||
if kind == "DEPTH":
|
||||
controls += ["Normal Smooth", "Smooth Weight", "Validity Threshold", "Split Threshold"]
|
||||
assert [item.name for item in panels[0].interface_items] == controls
|
||||
assert [item.name for item in panels[1].interface_items] == ["Uniform Scale", "Depth Image"]
|
||||
assert all(panel.default_closed for panel in panels)
|
||||
@@ -1,20 +1,14 @@
|
||||
"""Behavior checks for face separation and connected Depth Surface shells."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
import bpy
|
||||
import bmesh
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
|
||||
from node_asset_test_support import CUTOUT_BUILDERS
|
||||
from depth_surface_test_support import assert_closed, component_count, evaluated, surface
|
||||
from tests.support.nodes import CUTOUT_BUILDERS
|
||||
from tests.support.depth_surface import assert_closed, component_count, evaluated, surface
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", [0, 1])
|
||||
@@ -1,26 +1,13 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
|
||||
from anyimage.common import material
|
||||
from tools.nodes.groups.image_layer import build_image_layer_group
|
||||
from tests.support.materials import image_layer
|
||||
from tools.nodes.groups.image_depth_layer import build_image_depth_layer_group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_layer():
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
group = build_image_layer_group()
|
||||
yield group
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("image_layer", ["definition", "asset"], indirect=True)
|
||||
def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
|
||||
inputs = {
|
||||
item.name: item for item in image_layer.interface.items_tree
|
||||
@@ -61,28 +48,6 @@ def test_alpha_curve_preserves_endpoints_and_suppresses_soft_edges(image_layer):
|
||||
assert all(socket.is_linked or socket.hide for node in image_layer.nodes for socket in node.outputs)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled", [False, True])
|
||||
@pytest.mark.parametrize("shadeless", [False, True])
|
||||
@pytest.mark.parametrize("view", ["AgX", "Unknown"])
|
||||
def test_material_alpha_strength_follows_preference(image_layer, monkeypatch, enabled, shadeless, view):
|
||||
monkeypatch.setattr(material, "material_node_group", lambda depth_plane=False: image_layer)
|
||||
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: enabled)
|
||||
color = bpy.data.images.new("Alpha edge", width=2, height=2, alpha=True)
|
||||
scene = SimpleNamespace(view_settings=SimpleNamespace(view_transform=view))
|
||||
result = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
|
||||
layer = next(node for node in result.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
|
||||
assert layer.inputs["Alpha Fix"].default_value == float(enabled)
|
||||
alpha_link = layer.outputs["Alpha"].links[0]
|
||||
assert alpha_link.to_node.type == ("MIX_SHADER" if shadeless else "BSDF_PRINCIPLED")
|
||||
assert alpha_link.to_socket.name == ("Fac" if shadeless else "Alpha")
|
||||
layer.inputs["Alpha Fix"].default_value = 0.5
|
||||
monkeypatch.setattr(material, "configured_material_view_adaptation", lambda: not enabled)
|
||||
second = material.create_image_material(color, color, shadeless=shadeless, scene=scene)
|
||||
second_layer = next(node for node in second.node_tree.nodes if node.type == "GROUP" and node.node_tree == image_layer)
|
||||
assert second_layer.inputs["Alpha Fix"].default_value == float(not enabled)
|
||||
assert layer.inputs["Alpha Fix"].default_value == 0.5
|
||||
|
||||
|
||||
def test_depth_layer_keeps_foreground_alpha_threshold(image_layer):
|
||||
depth = build_image_depth_layer_group()
|
||||
assert "Alpha Fix" not in {
|
||||
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
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_ROOT = PROJECT_ROOT / "src" / "anyimage"
|
||||
|
||||
|
||||
@@ -10,7 +9,7 @@ class NodeAssetsTest(unittest.TestCase):
|
||||
def test_geometry_assets_preserve_node_structure(self):
|
||||
import bpy
|
||||
|
||||
from node_asset_test_support import validate_node_structure
|
||||
from tests.support.nodes import validate_node_structure
|
||||
from tools.nodes.arrangement.arrange import arrange_nodes
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
@@ -33,13 +32,6 @@ class NodeAssetsTest(unittest.TestCase):
|
||||
for actual, expected in zip(group.nodes[name].location, position):
|
||||
self.assertAlmostEqual(actual, expected, delta=0.01, msg=f"{group.name}: {name}")
|
||||
|
||||
def test_production_code_does_not_create_node_groups(self):
|
||||
offenders = []
|
||||
for path in SOURCE_ROOT.rglob("*.py"):
|
||||
if "bpy.data.node_groups.new" in path.read_text(encoding="utf-8"):
|
||||
offenders.append(path.relative_to(PROJECT_ROOT).as_posix())
|
||||
self.assertEqual(offenders, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,9 +1,6 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ToolCommandsTests(unittest.TestCase):
|
||||
@@ -15,7 +12,7 @@ class ToolCommandsTests(unittest.TestCase):
|
||||
project["scripts"],
|
||||
{
|
||||
"pack": "tools.pack:main",
|
||||
"build_node": "tools.nodes.cli:main",
|
||||
"build_nodes": "tools.nodes.cli:main",
|
||||
"sync_models": "tools.models.cli:main",
|
||||
"docs": "tools.docs:main",
|
||||
},
|
||||
@@ -1,8 +1,8 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parents[1] / "tools" / "docs.py"
|
||||
SCRIPT_PATH = PROJECT_ROOT / "tools" / "docs.py"
|
||||
SPEC = importlib.util.spec_from_file_location("docs_script", SCRIPT_PATH)
|
||||
docs = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(docs)
|
||||
@@ -1,9 +1,8 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
MANIFEST_PATH = PROJECT_ROOT / "src" / "anyimage" / "blender_manifest.toml"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from tests.support.paths import PROJECT_ROOT
|
||||
import importlib.util
|
||||
import tempfile
|
||||
import tomllib
|
||||
@@ -5,8 +6,6 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_pack():
|
||||
specification = importlib.util.spec_from_file_location(
|
||||
@@ -3,9 +3,12 @@
|
||||
import bpy
|
||||
|
||||
from ..common.nodes import group_input as _group_input, interface_socket as _interface_socket
|
||||
from ..common.nodes import prepare_node_group
|
||||
from ..common.nodes import prepare_node_group, float_input
|
||||
from ..common.smoothing import expand_smooth
|
||||
from ..common.depth_surface import project_depth_surface, thicken_depth_surface, remove_surface_attributes
|
||||
from ..common.depth_surface import (
|
||||
project_depth_surface, thicken_depth_surface, remove_surface_attributes,
|
||||
split_depth_surface, store_surface_camera,
|
||||
)
|
||||
|
||||
DEPTH_PLANE_NODE_GROUP_NAME = "O Image Depth Plane"
|
||||
|
||||
@@ -62,6 +65,7 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
validity_threshold.default_value = 0.9
|
||||
validity_threshold.min_value, validity_threshold.max_value = 0.0, 1.0
|
||||
validity_threshold.subtype = "FACTOR"
|
||||
float_input(group, "Split Threshold", 0.0, maximum=1, subtype="FACTOR", parent=options)
|
||||
data = group.interface.new_panel(name="Data", default_closed=True)
|
||||
uniform_scale = _interface_socket(
|
||||
group,
|
||||
@@ -99,33 +103,6 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
texture.interpolation, texture.extension = "Linear", "EXTEND"
|
||||
links.new(uv.outputs["Attribute"], texture.inputs["Vector"])
|
||||
links.new(texture_input.outputs["Depth Image"], texture.inputs["Image"])
|
||||
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
|
||||
surface = project_depth_surface(
|
||||
group, plane.outputs["Geometry"], texture.outputs["Color"],
|
||||
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
|
||||
depth_input.outputs["Depth Scale"],
|
||||
)
|
||||
shell_input = _group_input(nodes, {"Thickness", "Normal Smooth"})
|
||||
shell = thicken_depth_surface(
|
||||
group, surface, shell_input.outputs["Thickness"],
|
||||
normal_smooth=shell_input.outputs["Normal Smooth"],
|
||||
)
|
||||
positive = nodes.new("ShaderNodeMath")
|
||||
positive.operation = "GREATER_THAN"
|
||||
links.new(shell_input.outputs["Thickness"], positive.inputs[0])
|
||||
positive.inputs[1].default_value = 0
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(positive.outputs[0], choose.inputs["Switch"])
|
||||
links.new(surface, choose.inputs["False"])
|
||||
links.new(shell, choose.inputs["True"])
|
||||
clean = remove_surface_attributes(group, choose.outputs["Output"])
|
||||
# Preserve the repeated cap input independently of its display routing.
|
||||
cap = nodes.new("NodeReroute")
|
||||
links.new(nodes["Store Named Attribute.001"].outputs["Geometry"], cap.inputs[0])
|
||||
links.new(cap.outputs[0], nodes["Join Geometry"].inputs["Geometry"])
|
||||
projection_link = next(link for link in links if link.from_socket == plane.outputs["Geometry"])
|
||||
projection_input = projection_link.to_socket
|
||||
threshold_input = _group_input(nodes, {"Validity Threshold"})
|
||||
channels = nodes.new("FunctionNodeSeparateColor")
|
||||
channels.mode = "RGB"
|
||||
@@ -147,7 +124,35 @@ def build_image_depth_plane_group(image_plane_group):
|
||||
links.new(cull_controls.outputs["Valid Only"], cull.inputs["Switch"])
|
||||
links.new(delete.outputs["Geometry"], cull.inputs["True"])
|
||||
links.new(plane.outputs["Geometry"], cull.inputs["False"])
|
||||
links.new(cull.outputs["Output"], projection_input)
|
||||
split_input = _group_input(nodes, {"Split Threshold"})
|
||||
geometry = split_depth_surface(
|
||||
group, cull.outputs["Output"], texture_input.outputs["Depth Image"],
|
||||
split_input.outputs["Split Threshold"], scale_input.outputs["Uniform Scale"],
|
||||
)
|
||||
geometry, camera = store_surface_camera(
|
||||
group, geometry, texture.outputs["Color"], split_input.outputs["Split Threshold"],
|
||||
)
|
||||
depth_input = _group_input(nodes, {"Reference Depth", "Depth Scale"})
|
||||
surface = project_depth_surface(
|
||||
group, geometry, camera,
|
||||
scale_input.outputs["Uniform Scale"], depth_input.outputs["Reference Depth"],
|
||||
depth_input.outputs["Depth Scale"],
|
||||
)
|
||||
shell_input = _group_input(nodes, {"Thickness", "Normal Smooth"})
|
||||
shell = thicken_depth_surface(
|
||||
group, surface, shell_input.outputs["Thickness"],
|
||||
normal_smooth=shell_input.outputs["Normal Smooth"],
|
||||
)
|
||||
positive = nodes.new("ShaderNodeMath")
|
||||
positive.operation = "GREATER_THAN"
|
||||
links.new(shell_input.outputs["Thickness"], positive.inputs[0])
|
||||
positive.inputs[1].default_value = 0
|
||||
choose = nodes.new("GeometryNodeSwitch")
|
||||
choose.input_type = "GEOMETRY"
|
||||
links.new(positive.outputs[0], choose.inputs["Switch"])
|
||||
links.new(surface, choose.inputs["False"])
|
||||
links.new(shell, choose.inputs["True"])
|
||||
clean = remove_surface_attributes(group, choose.outputs["Output"])
|
||||
geometry = expand_smooth(
|
||||
group, clean, iterations_name="Smooth", weight_name="Smooth Weight",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user