Add geometry nodes asset and validation script

- Implemented a new geometry nodes asset in `build_geometry_nodes_asset.py` to handle mesh refinement and topology fixing.
- Created a validation script in `validate_geometry_nodes_asset.py` to ensure the integrity and functionality of the geometry node group.
- Added input handling for various parameters including resolution, iterations, and smoothing options.
- Included checks for node group structure and attributes to validate the geometry processing pipeline.
This commit is contained in:
2026-08-11 13:23:13 +08:00
parent fc27c23fd1
commit 07d836d9ad
15 changed files with 2408 additions and 68 deletions

View File

@@ -8,6 +8,8 @@ from tempfile import TemporaryDirectory
from types import ModuleType, SimpleNamespace
from unittest.mock import patch
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SOURCE_ROOT = PROJECT_ROOT / "src"
@@ -28,6 +30,7 @@ class AddonTest(unittest.TestCase):
)
fake_bpy.types = SimpleNamespace(
AddonPreferences=object,
Menu=object,
Operator=object,
Panel=object,
PropertyGroup=object,
@@ -79,6 +82,7 @@ class AddonTest(unittest.TestCase):
self.context_menu_draws.clear()
self.addon.operators._active_job = None
self.addon.image_depth._last_depth_plane_model = "BASE"
self.addon.image_mesh._last_mesh_model = "BASE"
self.fake_bpy.data = SimpleNamespace(
objects=SimpleNamespace(get=lambda _name: None),
)
@@ -125,6 +129,135 @@ class AddonTest(unittest.TestCase):
)
self.assertFalse(hasattr(self.addon, "ImportDepthPlane"))
def test_registers_perspective_mesh_operators(self):
self.assertIn(self.addon.ConvertImageToMesh, self.addon.CLASSES)
self.assertIn(self.addon.BuildImageMesh, self.addon.CLASSES)
self.assertEqual(self.addon.BuildImageMesh.bl_options, {"INTERNAL"})
self.assertIn(self.addon.SnapFormImageMenu, self.addon.CLASSES)
def test_image_context_menu_groups_actions_and_separates_background(self):
calls = []
class Layout:
operator_context = None
def operator(self, identifier, **_options):
calls.append(identifier)
def separator(self):
calls.append("separator")
menu = self.addon.SnapFormImageMenu()
menu.layout = Layout()
menu.draw(None)
self.assertEqual(
calls,
[
"snapform.convert_image_to_depth_plane",
"snapform.convert_image_to_mesh",
"separator",
"snapform.remove_image_background",
],
)
def test_image_mesh_reference_plane_matches_image_empty_bounds(self):
source_object = SimpleNamespace(
data=SimpleNamespace(size=(401, 201)),
empty_display_size=4.0,
empty_image_offset=(-0.25, -0.75),
)
intrinsics = np.asarray(
[[200.0, 0.0, 200.0], [0.0, 200.0, 100.0], [0.0, 0.0, 1.0]],
dtype=np.float32,
)
camera_vertices = np.asarray(
[
[-2.0, -1.0, 2.0],
[2.0, -1.0, 2.0],
[-2.0, 1.0, 2.0],
[2.0, 1.0, 2.0],
],
dtype=np.float32,
)
vertices, camera = self.addon.image_mesh.empty_anchored_vertices(
source_object,
camera_vertices,
intrinsics,
(401, 201),
2.0,
)
x_min, x_max, y_min, y_max = self.addon.image_depth.plane_bounds(
source_object
)
np.testing.assert_allclose(
vertices,
[
[x_min, y_max, 0.0],
[x_max, y_max, 0.0],
[x_min, y_min, 0.0],
[x_max, y_min, 0.0],
],
)
np.testing.assert_allclose(
camera["location"],
((x_min + x_max) * 0.5, (y_min + y_max) * 0.5, 2.0024922),
)
def test_mesh_conversion_dialog_only_keeps_model_and_camera_options(self):
annotations = self.addon.ConvertImageToMesh.__annotations__
for property_name in (
"snapform_confidence_percentile",
"snapform_edge_culling",
"snapform_face_reduction",
"snapform_depth_smoothing",
"snapform_remove_background",
"snapform_postprocess_method",
):
with self.subTest(property_name=property_name):
self.assertNotIn(property_name, annotations)
self.assertFalse(annotations["snapform_create_camera"]["default"])
self.assertFalse(annotations["snapform_unshaded"]["default"])
self.assertEqual(annotations["snapform_unshaded"]["name"], "Unshaded")
def test_image_mesh_uses_o_prefixed_attributes_and_bundled_nodes(self):
image_mesh = self.addon.image_mesh
names = set(image_mesh.POINT_ATTRIBUTES) | set(
image_mesh.FACE_ATTRIBUTES
)
self.assertTrue(all(name.startswith("o_") for name in names))
self.assertTrue(image_mesh.node_asset_path().is_file())
self.assertEqual(image_mesh.NODE_GROUP_NAME, "O Refine DA3 Mesh")
self.assertEqual(image_mesh.NODE_ASSET_NAME, "O_SnapForm.blend")
def test_image_mesh_material_depth_scale_matches_fitted_geometry(self):
mesh_object = SimpleNamespace(scale=SimpleNamespace(z=-2.0))
self.assertEqual(
self.addon.image_mesh.material_depth_scale(
mesh_object,
{"scale_z": 0.25},
),
0.5,
)
def test_image_mesh_default_resolution_uses_double_reduction_estimate(self):
vertices = np.asarray(
((0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)),
dtype=np.float32,
)
faces = np.asarray(((0, 1, 2),), dtype=np.int32)
edge_lengths = np.asarray((1.0, 1.0, np.sqrt(2.0)))
self.assertAlmostEqual(
self.addon.image_mesh.default_remesh_resolution(vertices, faces),
float(np.median(edge_lengths) * np.sqrt(10.0) * 2.0),
)
def test_registers_image_background_operators(self):
self.assertIn(self.addon.RemoveImageBackground, self.addon.CLASSES)
self.assertIn(self.addon.ApplyRemovedBackground, self.addon.CLASSES)
@@ -1486,6 +1619,51 @@ class AddonTest(unittest.TestCase):
)
self.assertEqual(parameters["use_ray_pose"], 0)
def test_reconstruct_request_enables_ben2_for_image_mesh(self):
runtime = importlib.import_module("snapform.runtime")
settings = SimpleNamespace(
input_path="input.png",
model="BASE",
gs_model="GIANT_1_1",
process_res=504,
process_res_method="upper_bound_resize",
conf_thresh_percentile=40.0,
num_max_points=1_000_000,
show_cameras=True,
gs_views_interval=1,
video_frames=16,
device="AUTO",
use_ray_pose=False,
ref_view_strategy="saddle_balanced",
)
with patch.object(
runtime,
"runtime_python",
return_value=Path("python"),
), patch.object(
runtime,
"model_directory",
side_effect=lambda key: Path(key.lower()),
):
request = runtime.reconstruct_request(
settings,
"mesh_data",
Path("status.json"),
Path("result"),
remove_background=True,
)
parameters = request.payload["parameters"]
self.assertNotIn("mesh_confidence_percentile", parameters)
self.assertNotIn("mesh_edge_culling", parameters)
self.assertNotIn("mesh_face_reduction", parameters)
self.assertNotIn("mesh_depth_smoothing", parameters)
self.assertNotIn("mesh_postprocess_method", parameters)
self.assertEqual(
parameters["remove_background_model_dir"],
str(Path("ben2_base")),
)
def test_export_format_property_includes_all_da3_formats(self):
export_format = self.addon.SnapFormSettings.__annotations__[
"export_format"