Add soft min and max constraints for frame resolution and cutout edge length properties
This commit is contained in:
@@ -3,7 +3,7 @@ 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
|
||||
from anyimage.operators.cutout_tool.mesh import build_mesh
|
||||
from anyimage.operators.cutout_tool.balloon import poisson_balloon_profile
|
||||
|
||||
|
||||
@@ -38,9 +38,8 @@ def test_depth_cutout_loads_uniform_thickness_revision_and_preserves_existing_gr
|
||||
assert load_cutout_node_group("DEPTH_SOLID") == current
|
||||
|
||||
|
||||
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))
|
||||
def test_small_quality_mesh_has_positive_balloon_support():
|
||||
points, faces, boundary, _ = build_mesh(np.ones((2, 2)), 32, .9)
|
||||
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)
|
||||
assert profile.max() > 0
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy.sparse import coo_matrix
|
||||
from scipy.sparse.csgraph import connected_components
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
from anyimage.operators.cutout_tool import geometry as g
|
||||
from anyimage.operators.cutout_tool.balloon import poisson_balloon_profile
|
||||
from anyimage.operators.cutout_tool import fine_mesh as fine
|
||||
|
||||
|
||||
def branch_alpha(offset=0):
|
||||
yy, xx = np.mgrid[:192, :192]
|
||||
xx = xx - offset
|
||||
yy = yy - offset
|
||||
alpha = (xx - 46) ** 2 + (yy - 48) ** 2 < 30**2
|
||||
path = np.asarray(((48, 48), (91, 70), (130, 110), (149, 167)))
|
||||
for a, b in zip(path[:-1], path[1:]):
|
||||
d = np.stack((xx - a[0], yy - a[1]), axis=-1)
|
||||
v = b - a
|
||||
t = np.clip(d @ v / (v @ v), 0, 1)
|
||||
alpha |= np.sum((d - t[..., None] * v) ** 2, axis=-1) < 2.5**2
|
||||
alpha |= (xx - 130) ** 2 + (yy - 110) ** 2 < 7**2
|
||||
return alpha.astype(float)
|
||||
|
||||
|
||||
def assert_supported(points, faces, boundary, components):
|
||||
triangles = points[faces]
|
||||
area = np.abs(
|
||||
np.cross(triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0])
|
||||
)
|
||||
assert np.isfinite(points).all() and np.all(area > 1e-8)
|
||||
assert not boundary[faces].all(axis=1).any()
|
||||
edges = np.sort(np.concatenate((faces[:, :2], faces[:, 1:], faces[:, ::2])), axis=1)
|
||||
edges, counts = np.unique(edges, axis=0, return_counts=True)
|
||||
assert counts.max() <= 2
|
||||
assert not boundary[edges[counts == 2]].all(axis=1).any()
|
||||
interior = np.flatnonzero(~boundary)
|
||||
links = edges[~boundary[edges].any(axis=1)]
|
||||
graph = coo_matrix(
|
||||
(np.ones(len(links)), (links[:, 0], links[:, 1])),
|
||||
shape=(len(points), len(points)),
|
||||
).tocsr()
|
||||
count, _labels = connected_components(graph[interior][:, interior], directed=False)
|
||||
assert count == len(components)
|
||||
profile = poisson_balloon_profile(points, faces, boundary)
|
||||
assert np.isfinite(profile).all()
|
||||
assert np.all(profile[interior] > 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spacing", (4, 8, 16, 28, 32, 36))
|
||||
@pytest.mark.parametrize("offset", (0, 1))
|
||||
def test_fine_bent_leg_and_joint_have_connected_positive_support(spacing, offset):
|
||||
points, faces, boundary, components = fine.build_fine_mesh(
|
||||
branch_alpha(offset), spacing, 0.9
|
||||
)
|
||||
assert_supported(points, faces, boundary, components)
|
||||
interior = points[~boundary]
|
||||
for checkpoint in ((90, 70), (130, 110), (146, 159)):
|
||||
assert cKDTree(interior).query(np.asarray(checkpoint) + offset)[0] < 12
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spacing", (4, 8, 16, 32))
|
||||
def test_fine_comb_hole_and_one_pixel_bridge(spacing):
|
||||
alpha = np.zeros((160, 180))
|
||||
alpha[15:145, 15:42] = 1
|
||||
alpha[45:115, 125:165] = 1
|
||||
alpha[75, 40:126] = 1
|
||||
alpha[65:90, 140:150] = 0
|
||||
for row in range(20, 140, 12):
|
||||
alpha[row : row + 2, 35:110] = 1
|
||||
result = fine.build_fine_mesh(alpha, spacing, 0.9)
|
||||
assert_supported(*result)
|
||||
points, faces, _boundary, components = result
|
||||
assert len(components) == 1
|
||||
assert not np.any(
|
||||
g.points_inside_contours(
|
||||
points[faces].mean(axis=1),
|
||||
[np.asarray(((140, 65), (150, 65), (150, 90), (140, 90)))],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_fine_cleanup_uses_threshold_and_preserves_input_and_largest_island():
|
||||
alpha = np.zeros((40, 60))
|
||||
alpha[5:25, 5:25] = 0.5
|
||||
alpha[12, 20:45] = 0.5
|
||||
alpha[32:34, 50:52] = 1
|
||||
original = alpha.copy()
|
||||
filtered, stats = fine.filter_alpha(alpha, 0.3)
|
||||
assert stats["components_after"] == 1
|
||||
assert filtered[12, 44] == 0.5 and filtered[32, 50] == 0
|
||||
np.testing.assert_array_equal(alpha, original)
|
||||
assert_supported(*fine.build_fine_mesh(alpha, 32, 0.3))
|
||||
assert_supported(*fine.build_fine_mesh(alpha, 32, 0.9))
|
||||
with pytest.raises(ValueError, match="no visible"):
|
||||
fine.build_fine_mesh(np.zeros((10, 10)), 32, 0.9)
|
||||
|
||||
|
||||
def test_higher_detail_recovers_curve_instead_of_splitting_low_edges():
|
||||
yy, xx = np.mgrid[:240, :240]
|
||||
alpha = ((xx - 120) ** 2 + (yy - 120) ** 2 < 92**2).astype(float)
|
||||
reference = g.extract_selection_contours(alpha, max_resolution=240, threshold=0.9)
|
||||
source = g.resample_closed_contour(np.asarray(reference.components[0][0]), 0.5)
|
||||
errors = []
|
||||
for spacing in (32, 8, 4):
|
||||
contours = fine.contours_for_component(reference.components[0], spacing)
|
||||
sampled = g.resample_closed_contour(contours[0], 0.5)
|
||||
forward = fine.ContourDomain(contours).clearance(source)
|
||||
backward = fine.ContourDomain(
|
||||
[np.asarray(reference.components[0][0])]
|
||||
).clearance(sampled)
|
||||
errors.append(max(np.percentile(forward, 95), np.percentile(backward, 95)))
|
||||
assert errors[1] < errors[0]
|
||||
assert errors[2] <= errors[1] + 0.1
|
||||
|
||||
|
||||
def test_segment_validation_rejects_holes_tangency_and_outside():
|
||||
domain = fine.ContourDomain(
|
||||
[
|
||||
np.asarray(((0, 0), (20, 0), (20, 20), (0, 20))),
|
||||
np.asarray(((8, 8), (12, 8), (12, 12), (8, 12))),
|
||||
]
|
||||
)
|
||||
starts = np.asarray(((2, 2), (2, 10), (2, 8), (-1, 2), (2, 2)))
|
||||
ends = np.asarray(((18, 2), (18, 10), (18, 8), (2, 2), (2, 2)))
|
||||
assert domain.valid_segments(starts, ends).tolist() == [
|
||||
True,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
True,
|
||||
]
|
||||
|
||||
|
||||
def test_closed_mode_uses_original_geometry_without_fine_import(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
from mathutils import Matrix
|
||||
from anyimage.common.image import image_empty_bounds
|
||||
|
||||
source = SimpleNamespace(
|
||||
data=SimpleNamespace(size=(80, 60)),
|
||||
empty_display_size=2.0,
|
||||
empty_image_offset=(-0.5, -0.5),
|
||||
matrix_world=Matrix.Identity(4),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
g,
|
||||
"addon_preferences",
|
||||
lambda _context: SimpleNamespace(balloon_profile_algorithm="POISSON"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
fine,
|
||||
"build_fine_mesh",
|
||||
lambda *_args: pytest.fail("Fine algorithm called while disabled"),
|
||||
)
|
||||
alpha = np.ones((60, 80))
|
||||
actual = g.build_base_shape(
|
||||
None, source, 0.4, alpha, (0, 0, 80, 60), fine_outline=False
|
||||
)
|
||||
contours = g.extract_cutout_contours(alpha, 5, threshold=0.9)
|
||||
expected = g.cutout_domain(
|
||||
contours,
|
||||
image_empty_bounds(source),
|
||||
0.4,
|
||||
((1.0, 0.0), (0.0, 1.0)),
|
||||
)
|
||||
np.testing.assert_array_equal(actual.vertices, expected[0])
|
||||
np.testing.assert_array_equal(actual.uv, expected[2])
|
||||
np.testing.assert_array_equal(actual.balloon, expected[-1])
|
||||
|
||||
|
||||
def test_width_index_matches_exhaustive_covering_disks():
|
||||
from scipy import ndimage
|
||||
|
||||
width = fine.WidthSamples(branch_alpha().astype(bool))
|
||||
points = np.asarray(((48, 48), (85, 70), (130, 110), (147, 160), (2, 2)))
|
||||
radii = []
|
||||
for point in points:
|
||||
covered = np.sum((width.centers - point) ** 2, axis=1) <= width.reach_squared
|
||||
radii.append(max(0.5, width.radius[covered].max() if covered.any() else 0.5))
|
||||
distances = ndimage.map_coordinates(
|
||||
width.distance,
|
||||
[points[:, 1] - 0.5, points[:, 0] - 0.5],
|
||||
order=1,
|
||||
mode="nearest",
|
||||
)
|
||||
expected = np.maximum(
|
||||
0.7, np.minimum(32, np.minimum(16, np.asarray(radii) * 0.9) + 0.4 * distances)
|
||||
)
|
||||
np.testing.assert_allclose(width(points), expected)
|
||||
|
||||
|
||||
def test_relaxation_reduces_sampling_error_without_adding_points():
|
||||
contours = [np.asarray(((4.0, 4.0), (194.0, 4.0), (194.0, 104.0), (4.0, 104.0)))]
|
||||
spine = np.empty((0, 2))
|
||||
original = g.uniform_interior_points_for_contours(contours, 16)
|
||||
relaxed = fine.relax_points(contours, spine, 16)
|
||||
yy, xx = np.mgrid[6:104:4, 6:194:4]
|
||||
samples = np.column_stack((xx.ravel() + 0.5, yy.ravel() + 0.5))
|
||||
|
||||
def energy(free):
|
||||
return np.mean(
|
||||
cKDTree(np.concatenate((*contours, free))).query(samples)[0] ** 2
|
||||
)
|
||||
|
||||
assert len(relaxed) == len(original)
|
||||
assert energy(relaxed) < energy(original)
|
||||
assert fine.ContourDomain(contours).contains(relaxed).all()
|
||||
|
||||
|
||||
def test_cdt_preserves_constraint_split_by_an_interior_vertex():
|
||||
contours = [np.asarray(((0.0, 0.0), (20.0, 0.0), (20.0, 10.0), (0.0, 10.0)))]
|
||||
spine = np.asarray(((2.0, 5.0), (18.0, 5.0)))
|
||||
points, faces, constraints = fine.triangulate(
|
||||
contours, spine, [(0, 1)], np.asarray(((10.0, 5.0),)), 8
|
||||
)
|
||||
assert len(constraints) == 2
|
||||
assert np.isclose(
|
||||
np.linalg.norm(
|
||||
points[constraints[:, 1]] - points[constraints[:, 0]], axis=1
|
||||
).sum(),
|
||||
16,
|
||||
)
|
||||
assert not g.boundary_vertices_from_faces(len(points), faces)[constraints].any()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("offset", [0, 4096, 16384])
|
||||
def test_cdt_collinear_outline_samples_do_not_become_sliver_faces(offset):
|
||||
contour = np.asarray(((0., 0.), (240., 0.), (240., 160.), (0., 160.)))
|
||||
contour = contour @ np.asarray(((.763, -.646), (.646, .763)))
|
||||
contour += np.asarray((420.123456, 540.234567)) + offset
|
||||
contours, spine, edges = fine.subdivide_constraints(
|
||||
[contour], np.empty((0, 2)), [], 16
|
||||
)
|
||||
free = g.uniform_interior_points_for_contours(contours, 16)
|
||||
points, faces, _ = fine.triangulate(contours, spine, edges, free, 16)
|
||||
triangles = points[faces]
|
||||
area = abs(np.cross(triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0]))
|
||||
longest_squared = np.max(np.sum((triangles - np.roll(triangles, 1, axis=1))**2, axis=2), axis=1)
|
||||
assert np.min(area / longest_squared) > 1e-4
|
||||
assert area.sum() / 2 == pytest.approx(abs(g.contour_area(contour)), rel=1e-9)
|
||||
boundary = g.boundary_vertices_from_faces(len(points), faces)
|
||||
np.testing.assert_allclose(fine.ContourDomain([contour]).clearance(points[boundary]), 0, atol=1e-9)
|
||||
# Every subdivided outline point survives; removing slivers must not skip it.
|
||||
assert cKDTree(points[boundary]).query(contours[0])[0].max() < 1e-9
|
||||
|
||||
|
||||
def test_contour_validation_accepts_pixel_contacts_and_rejects_crossings():
|
||||
crossing = np.asarray(((0, 0), (4, 4), (0, 4), (4, 0)))
|
||||
assert not fine.valid_contours([crossing])
|
||||
touching = np.asarray(
|
||||
((0, 0), (2, 0), (2, 2), (4, 2), (4, 4), (2, 4), (2, 2), (0, 2))
|
||||
)
|
||||
assert fine.valid_contours([touching])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("offset", [0, 2048, 4096, 8192, 16384])
|
||||
@pytest.mark.parametrize("spacing", [4, 0.5])
|
||||
def test_cdt_preserves_short_constraints_at_large_pixel_coordinates(offset, spacing):
|
||||
contour = np.asarray(((0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0))) + offset
|
||||
spine = np.asarray(((5.1234567, 5.654321), (6.456789, 6.234567))) + offset
|
||||
free = g.uniform_interior_points_for_contours([contour], spacing)
|
||||
points, faces, constraints = fine.triangulate(
|
||||
[contour], spine, [(0, 1)], free, spacing
|
||||
)
|
||||
assert len(constraints) >= 1
|
||||
assert not g.boundary_vertices_from_faces(len(points), faces)[constraints].any()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"damage", ["missing_edge", "missing_faces", "missing_endpoint"]
|
||||
)
|
||||
def test_cdt_rejects_broken_constraint_coverage(monkeypatch, damage):
|
||||
real_cdt = fine.delaunay_2d_cdt
|
||||
|
||||
def damaged_cdt(*args):
|
||||
result = list(real_cdt(*args))
|
||||
result[2] = list(result[2])
|
||||
result[3] = [list(ids) for ids in result[3]]
|
||||
result[4] = [list(ids) for ids in result[4]]
|
||||
constraint = len(args[1]) - 1
|
||||
edge_id = next(i for i, ids in enumerate(result[4]) if constraint in ids)
|
||||
if damage == "missing_edge":
|
||||
result[4][edge_id].remove(constraint)
|
||||
elif damage == "missing_faces":
|
||||
a, b = result[1][edge_id]
|
||||
result[2] = [face for face in result[2] if not (a in face and b in face)]
|
||||
else:
|
||||
endpoint = args[1][-1][1]
|
||||
for originals in result[3]:
|
||||
if endpoint in originals:
|
||||
originals.remove(endpoint)
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(fine, "delaunay_2d_cdt", damaged_cdt)
|
||||
contour = np.asarray(((0.0, 0.0), (20.0, 0.0), (20.0, 10.0), (0.0, 10.0)))
|
||||
spine = np.asarray(((2.0, 5.0), (18.0, 5.0)))
|
||||
with pytest.raises(ValueError, match="lost an interior constraint"):
|
||||
fine.triangulate([contour], spine, [(0, 1)], np.asarray(((10.0, 5.0),)), 8)
|
||||
@@ -175,7 +175,7 @@ def test_cutout_cleanup_value_scales_the_ui_without_changing_node_area():
|
||||
def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_selection():
|
||||
components = ((((2046, 2046), (2050, 2046), (2050, 2050), (2046, 2050)),),)
|
||||
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
domain = quality_domain(
|
||||
SelectionContours(components, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -189,7 +189,7 @@ def test_cutout_domain_adapts_boundary_spacing_for_a_small_valid_selection():
|
||||
def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
components = ((((0, 0), (4096, 0), (4096, 4096), (0, 4096)),),)
|
||||
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
domain = quality_domain(
|
||||
SelectionContours(components, (4096, 4096)),
|
||||
(-1.0, 1.0, -1.0, 1.0),
|
||||
0.5,
|
||||
@@ -203,7 +203,7 @@ def test_cutout_domain_builds_a_full_image_at_minimum_detail():
|
||||
def test_cutout_domain_centers_vertices_on_crop_bounds():
|
||||
components = ((((0, 0), (100, 0), (100, 50), (0, 50)),),)
|
||||
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = cutout_geometry.cutout_domain(
|
||||
vertices, _faces, uv, _boundary, _planar, _profile = quality_domain(
|
||||
SelectionContours(components, (100, 50)),
|
||||
(2.0, 6.0, -3.0, 1.0),
|
||||
0.5,
|
||||
@@ -224,7 +224,7 @@ def test_cutout_domain_keeps_overlapping_selection_components_disconnected():
|
||||
),
|
||||
(((0, 20), (40, 20), (40, 50), (0, 50)),),
|
||||
)
|
||||
domain = cutout_geometry.cutout_domain(
|
||||
domain = quality_domain(
|
||||
SelectionContours(components, (120, 80)),
|
||||
(0.0, 1.0, 0.0, 1.0),
|
||||
0.025,
|
||||
@@ -286,3 +286,19 @@ class CutoutContourTest(unittest.TestCase):
|
||||
return float(np.std(points[selected, 1] - points[selected, 0]))
|
||||
|
||||
self.assertLess(diagonal_noise(smooth), 0.05)
|
||||
|
||||
|
||||
def quality_domain(selection, bounds, spacing, metric):
|
||||
from anyimage.operators.cutout_tool import mesh
|
||||
ps, fs, cs = [], [], []
|
||||
offset = 0
|
||||
width, height = selection.image_size
|
||||
pixel_scale = max((bounds[1]-bounds[0])/width, (bounds[3]-bounds[2])/height)
|
||||
for component in selection.components:
|
||||
contours = [np.array(c, dtype=float) for c in component]
|
||||
p, f = mesh.triangulate(contours, spacing/pixel_scale)
|
||||
p, f = mesh.support_mesh(p, f)
|
||||
ps.append(p);fs.append(f+offset)
|
||||
cs.append((offset, offset+len(p), f, contours[0]));offset += len(p)
|
||||
p, f = np.concatenate(ps), np.concatenate(fs)
|
||||
return cutout_geometry.cutout_domain((p, f, cutout_geometry.boundary_vertices_from_faces(len(p), f), cs, selection.image_size), bounds, metric)
|
||||
|
||||
334
tests/anyimage/operators/test_cutout_mesh.py
Normal file
334
tests/anyimage/operators/test_cutout_mesh.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from scipy.sparse import coo_matrix
|
||||
from scipy.sparse.csgraph import connected_components
|
||||
from scipy.spatial import cKDTree
|
||||
|
||||
from anyimage.operators.cutout_tool import geometry as g
|
||||
from anyimage.operators.cutout_tool.balloon import poisson_balloon_profile
|
||||
from anyimage.operators.cutout_tool import mesh
|
||||
|
||||
|
||||
def boundary_distance(points, contours):
|
||||
starts = np.concatenate(contours)
|
||||
delta = np.concatenate([np.roll(c, -1, axis=0) - c for c in contours])
|
||||
offset = points[:, None] - starts
|
||||
fraction = np.clip(np.sum(offset * delta, axis=2) / np.sum(delta**2, axis=1), 0, 1)
|
||||
return np.linalg.norm(offset - fraction[..., None] * delta, axis=2).min(axis=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fine_outline", [False, True])
|
||||
@pytest.mark.parametrize("spacing", [2, 12, 48, 96])
|
||||
def test_local_support_connects_coarse_and_dense_branches(fine_outline, spacing):
|
||||
result = mesh.build_mesh(branch_alpha(), spacing, 0.9, fine_outline=fine_outline)
|
||||
assert_supported(*result)
|
||||
|
||||
|
||||
def test_support_splits_shared_chords_and_preserves_boundary_and_area():
|
||||
points = np.array(((0., 0.), (20., 0.), (20., 2.), (0., 2.)))
|
||||
faces = np.array(((0, 1, 2), (0, 2, 3)))
|
||||
p, f = mesh.support_mesh(points, faces)
|
||||
assert len(p) == 5 and len(f) == 4
|
||||
assert_supported(p, f, g.boundary_vertices_from_faces(len(p), f), [None])
|
||||
triangles = p[f]
|
||||
assert np.cross(triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0]).sum() == pytest.approx(80)
|
||||
np.testing.assert_array_equal(p[:4], points)
|
||||
with pytest.raises(ValueError, match="limit"):
|
||||
mesh.support_mesh(points, faces, max_vertices=4)
|
||||
|
||||
|
||||
def test_support_adds_one_sample_to_isolated_triangle_and_is_idempotent():
|
||||
points = np.array(((0., 0.), (20., 0.), (0., 2.)))
|
||||
faces = np.array(((0, 1, 2),))
|
||||
p, f = mesh.support_mesh(points, faces)
|
||||
assert len(p) == 4 and len(f) == 3
|
||||
assert_supported(p, f, g.boundary_vertices_from_faces(len(p), f), [None])
|
||||
again_p, again_f = mesh.support_mesh(p, f)
|
||||
np.testing.assert_array_equal(p, again_p)
|
||||
np.testing.assert_array_equal(f, again_f)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spacing", [1, 4, 16])
|
||||
def test_pixel_corner_hole_contacts_produce_valid_supported_mesh(spacing):
|
||||
alpha = np.ones((40, 40))
|
||||
alpha[10:17, 10:17] = 0
|
||||
alpha[17:24, 17:24] = 0
|
||||
original = alpha.copy()
|
||||
result = mesh.build_mesh(alpha, spacing, .9)
|
||||
assert_supported(*result)
|
||||
np.testing.assert_array_equal(alpha, original)
|
||||
p, f, b, _ = result
|
||||
edges, counts = np.unique(np.sort(np.concatenate((f[:, :2], f[:, 1:], f[:, ::2])), axis=1), axis=0, return_counts=True)
|
||||
assert np.all(np.bincount(edges[counts == 1].ravel(), minlength=len(p))[b] == 2)
|
||||
centers = p[f].mean(axis=1)
|
||||
# The pixel-scale smoothing may round the hole corners; its interior stays open.
|
||||
assert not np.any((centers[:, 0] > 11) & (centers[:, 0] < 16) & (centers[:, 1] > 11) & (centers[:, 1] < 16))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("damage", ["missing_edge", "missing_faces", "missing_endpoint"])
|
||||
def test_quality_mesh_rejects_broken_boundary_coverage(monkeypatch, damage):
|
||||
from anyimage.operators.cutout_tool import tangle
|
||||
|
||||
original = tangle.build_mesh
|
||||
def damaged(*args, **kwargs):
|
||||
points, faces, segments = original(*args, **kwargs)
|
||||
if damage == "missing_edge":
|
||||
segments = [segment for segment in segments if segment[2] != 1]
|
||||
elif damage == "missing_faces":
|
||||
faces = faces[1:]
|
||||
else:
|
||||
_, b, _ = next(segment for segment in segments if segment[2] == 1)
|
||||
points[b] = (points[b][0] + .05, points[b][1] + .05)
|
||||
return points, faces, segments
|
||||
monkeypatch.setattr(tangle, "build_mesh", damaged)
|
||||
contour = np.array(((0., 0.), (20., 0.), (20., 10.), (0., 10.)))
|
||||
with pytest.raises(ValueError):
|
||||
mesh.triangulate([contour], 8)
|
||||
|
||||
|
||||
def branch_alpha(offset=0):
|
||||
yy, xx = np.mgrid[:192, :192]
|
||||
xx = xx - offset
|
||||
yy = yy - offset
|
||||
alpha = (xx - 46) ** 2 + (yy - 48) ** 2 < 30**2
|
||||
path = np.asarray(((48, 48), (91, 70), (130, 110), (149, 167)))
|
||||
for a, b in zip(path[:-1], path[1:]):
|
||||
d = np.stack((xx - a[0], yy - a[1]), axis=-1)
|
||||
v = b - a
|
||||
t = np.clip(d @ v / (v @ v), 0, 1)
|
||||
alpha |= np.sum((d - t[..., None] * v) ** 2, axis=-1) < 2.5**2
|
||||
alpha |= (xx - 130) ** 2 + (yy - 110) ** 2 < 7**2
|
||||
return alpha.astype(float)
|
||||
|
||||
|
||||
def assert_supported(points, faces, boundary, components):
|
||||
triangles = points[faces]
|
||||
area = np.abs(
|
||||
np.cross(triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0])
|
||||
)
|
||||
assert np.isfinite(points).all() and np.all(area > 1e-8)
|
||||
edges = np.sort(np.concatenate((faces[:, :2], faces[:, 1:], faces[:, ::2])), axis=1)
|
||||
edges, counts = np.unique(edges, axis=0, return_counts=True)
|
||||
assert counts.max() <= 2
|
||||
interior = np.flatnonzero(~boundary)
|
||||
links = edges[~boundary[edges].any(axis=1)]
|
||||
graph = coo_matrix(
|
||||
(np.ones(len(links)), (links[:, 0], links[:, 1])),
|
||||
shape=(len(points), len(points)),
|
||||
).tocsr()
|
||||
count, _labels = connected_components(graph[interior][:, interior], directed=False)
|
||||
assert count == len(components)
|
||||
assert not np.any(boundary[faces].all(axis=1))
|
||||
assert len(interior) >= len(components)
|
||||
profile = poisson_balloon_profile(points, faces, boundary)
|
||||
assert np.isfinite(profile).all()
|
||||
assert np.all(profile[interior] > 0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spacing", (4, 8, 16, 28, 32, 36))
|
||||
@pytest.mark.parametrize("offset", (0, 1))
|
||||
def test_fine_bent_leg_and_joint_have_connected_positive_support(spacing, offset):
|
||||
points, faces, boundary, components = mesh.build_mesh(
|
||||
branch_alpha(offset), spacing, 0.9
|
||||
)
|
||||
assert_supported(points, faces, boundary, components)
|
||||
interior = points[~boundary]
|
||||
checkpoints = np.array(((48, 48), (90, 70), (130, 110), (146, 159))) + offset
|
||||
distance, nearest = cKDTree(interior).query(checkpoints)
|
||||
assert distance.max() < 12
|
||||
edges = np.unique(
|
||||
np.sort(np.concatenate((faces[:, :2], faces[:, 1:], faces[:, ::2])), axis=1),
|
||||
axis=0,
|
||||
)
|
||||
links = edges[~boundary[edges].any(1)]
|
||||
graph = coo_matrix(
|
||||
(np.ones(len(links)), (links[:, 0], links[:, 1])),
|
||||
shape=(len(points), len(points)),
|
||||
).tocsr()
|
||||
_, labels = connected_components(graph, directed=False)
|
||||
assert len(set(labels[np.flatnonzero(~boundary)[nearest]])) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spacing", (4, 8, 16, 32))
|
||||
def test_fine_comb_hole_and_one_pixel_bridge(spacing):
|
||||
alpha = np.zeros((160, 180))
|
||||
alpha[15:145, 15:42] = 1
|
||||
alpha[45:115, 125:165] = 1
|
||||
alpha[75, 40:126] = 1
|
||||
alpha[65:90, 140:150] = 0
|
||||
for row in range(20, 140, 12):
|
||||
alpha[row : row + 2, 35:110] = 1
|
||||
result = mesh.build_mesh(alpha, spacing, 0.9)
|
||||
assert_supported(*result)
|
||||
points, faces, _boundary, components = result
|
||||
assert len(components) == 1
|
||||
assert not np.any(
|
||||
g.points_inside_contours(
|
||||
points[faces].mean(axis=1),
|
||||
[np.asarray(((140, 65), (150, 65), (150, 90), (140, 90)))],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_fine_cleanup_uses_threshold_and_preserves_input_and_largest_island():
|
||||
alpha = np.zeros((40, 60))
|
||||
alpha[5:25, 5:25] = 0.5
|
||||
alpha[12, 20:45] = 0.5
|
||||
alpha[32:34, 50:52] = 1
|
||||
original = alpha.copy()
|
||||
filtered = mesh.filter_alpha(alpha, 0.3)
|
||||
assert filtered[12, 44] == 0.5 and filtered[32, 50] == 0
|
||||
np.testing.assert_array_equal(alpha, original)
|
||||
assert_supported(*mesh.build_mesh(alpha, 32, 0.3))
|
||||
assert_supported(*mesh.build_mesh(alpha, 32, 0.9))
|
||||
with pytest.raises(ValueError, match="no visible"):
|
||||
mesh.build_mesh(np.zeros((10, 10)), 32, 0.9)
|
||||
|
||||
|
||||
def test_higher_detail_recovers_curve_instead_of_splitting_low_edges():
|
||||
yy, xx = np.mgrid[:240, :240]
|
||||
alpha = ((xx - 120) ** 2 + (yy - 120) ** 2 < 92**2).astype(float)
|
||||
reference = g.extract_selection_contours(alpha, max_resolution=240, threshold=0.9)
|
||||
source = g.resample_closed_contour(np.asarray(reference.components[0][0]), 0.5)
|
||||
errors = []
|
||||
for spacing in (32, 8, 4):
|
||||
contours = mesh.contours_for_component(reference.components[0], spacing)
|
||||
sampled = g.resample_closed_contour(contours[0], 0.5)
|
||||
forward = boundary_distance(source, contours)
|
||||
backward = boundary_distance(sampled, [np.asarray(reference.components[0][0])])
|
||||
errors.append(max(np.percentile(forward, 95), np.percentile(backward, 95)))
|
||||
assert errors[1] < errors[0]
|
||||
assert errors[2] <= errors[1] + 0.1
|
||||
|
||||
|
||||
def test_fine_outline_softens_pixel_steps_in_normalized_coordinates():
|
||||
yy, xx = np.mgrid[:120, :120]
|
||||
alpha = ((xx - 60)**2 + (yy - 60)**2 < 45**2).astype(float)
|
||||
source = np.asarray(g.extract_selection_contours(alpha, max_resolution=120).components[0][0])
|
||||
transform = np.diag((8., 12.))
|
||||
contours = mesh.contours_for_component([source @ transform], 32, pixel_transform=transform)
|
||||
smoothed = contours[0] @ np.linalg.inv(transform)
|
||||
|
||||
def turning(points):
|
||||
edges = np.roll(points, -1, axis=0) - points
|
||||
angles = np.arctan2(edges[:, 1], edges[:, 0])
|
||||
change = (np.roll(angles, -1) - angles + np.pi) % (2*np.pi) - np.pi
|
||||
return abs(change).sum()
|
||||
|
||||
assert turning(smoothed) < turning(source) * .5
|
||||
assert boundary_distance(smoothed, [source]).max() < 1
|
||||
assert abs(g.contour_area(smoothed) / g.contour_area(source) - 1) < .02
|
||||
|
||||
|
||||
@pytest.mark.parametrize("offset", [0, 4096, 16384])
|
||||
def test_cdt_collinear_outline_samples_do_not_become_sliver_faces(offset):
|
||||
contour = np.asarray(((0.0, 0.0), (240.0, 0.0), (240.0, 160.0), (0.0, 160.0)))
|
||||
contour = contour @ np.asarray(((0.763, -0.646), (0.646, 0.763)))
|
||||
contour += np.asarray((420.123456, 540.234567)) + offset
|
||||
contours = [g.resample_closed_contour(contour, 16)]
|
||||
points, faces = mesh.triangulate(contours, 16)
|
||||
triangles = points[faces]
|
||||
area = abs(
|
||||
np.cross(triangles[:, 1] - triangles[:, 0], triangles[:, 2] - triangles[:, 0])
|
||||
)
|
||||
longest_squared = np.max(
|
||||
np.sum((triangles - np.roll(triangles, 1, axis=1)) ** 2, axis=2), axis=1
|
||||
)
|
||||
assert np.min(area / longest_squared) > 1e-4
|
||||
assert area.sum() / 2 == pytest.approx(abs(g.contour_area(contour)), rel=1e-9)
|
||||
boundary = g.boundary_vertices_from_faces(len(points), faces)
|
||||
np.testing.assert_allclose(
|
||||
boundary_distance(points[boundary], [contour]), 0, atol=1e-9
|
||||
)
|
||||
# Every subdivided outline point survives; removing slivers must not skip it.
|
||||
assert cKDTree(points[boundary]).query(contours[0])[0].max() < 1e-9
|
||||
|
||||
|
||||
def test_contour_validation_requires_separation_of_pixel_contacts():
|
||||
crossing = np.asarray(((0, 0), (4, 4), (0, 4), (4, 0)))
|
||||
assert not mesh.valid_contours([crossing])
|
||||
touching = np.asarray(
|
||||
((0, 0), (2, 0), (2, 2), (4, 2), (4, 4), (2, 4), (2, 2), (0, 2))
|
||||
)
|
||||
assert not mesh.valid_contours([touching])
|
||||
assert mesh.valid_contours(mesh.separate_contour_contacts([touching.astype(float)]))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fine_outline", [False, True])
|
||||
def test_hole_policy_preserves_alpha_and_external_notches(fine_outline):
|
||||
alpha = np.ones((80, 80))
|
||||
alpha[10:12, 10:12] = 0
|
||||
alpha[20:29, 20:29] = 0
|
||||
alpha[40:41, 10:40] = 0
|
||||
alpha[:15, 60:65] = 0
|
||||
original = alpha.copy()
|
||||
mask = mesh.prepare_mask(alpha, 0.9, np.eye(2) * 3.2, fine_outline)
|
||||
assert mask[10, 10]
|
||||
assert mask[24, 24] == (not fine_outline)
|
||||
assert mask[40, 25] == (not fine_outline)
|
||||
assert not mask[0, 60]
|
||||
np.testing.assert_array_equal(alpha, original)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size,filled", [(2, True), (3, False)])
|
||||
def test_micro_hole_threshold_respects_world_transform(size, filled):
|
||||
alpha = np.ones((40, 40))
|
||||
alpha[10 : 10 + size, 10 : 10 + size] = 0
|
||||
mask = mesh.prepare_mask(alpha, 0.9, np.eye(2) * 3.2, True)
|
||||
assert bool(mask[10, 10]) == filled
|
||||
stretched = mesh.prepare_mask(alpha, 0.9, np.diag((6.4, 3.2)), True)
|
||||
assert not stretched[10, 10]
|
||||
|
||||
|
||||
def test_filling_nested_hole_merges_island_without_overlap():
|
||||
alpha = np.zeros((80, 80))
|
||||
alpha[5:75, 5:75] = 1
|
||||
alpha[20:60, 20:60] = 0
|
||||
alpha[30:50, 30:50] = 1
|
||||
result = mesh.build_mesh(alpha, 8, 0.9, fine_outline=False)
|
||||
p, f, _, components = result
|
||||
assert len(components) == 1
|
||||
assert_supported(*result)
|
||||
edges = np.unique(
|
||||
np.sort(np.concatenate((f[:, :2], f[:, 1:], f[:, ::2])), axis=1), axis=0
|
||||
)
|
||||
assert 1 - len(p) + len(edges) - len(f) == 0
|
||||
|
||||
|
||||
def test_tangle_numeric_context_and_failure_recovery():
|
||||
from decimal import getcontext
|
||||
from anyimage.operators.cutout_tool import tangle
|
||||
|
||||
points = [(0.0, 0.0), (20.0, 0.0), (20.0, 20.0), (0.0, 20.0)]
|
||||
segments = [(0, 1, 1), (1, 2, 2), (2, 3, 3), (3, 0, 4)]
|
||||
before = getcontext().copy()
|
||||
epsilon = tangle.EPS
|
||||
first = tangle.build_mesh(points, segments, [], 10.0)
|
||||
with pytest.raises(ValueError, match="limit"):
|
||||
tangle.build_mesh(points, segments, [], 0.01, max_vertices=12)
|
||||
assert getcontext().prec == before.prec and getcontext().flags == before.flags
|
||||
assert tangle.EPS == epsilon
|
||||
assert first == tangle.build_mesh(points, segments, [], 10.0)
|
||||
|
||||
|
||||
def test_large_hole_grid_stops_at_vertex_limit_and_recovers():
|
||||
from anyimage.operators.cutout_tool import tangle
|
||||
|
||||
points = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]
|
||||
segments = [(0, 1, 1), (1, 2, 2), (2, 3, 3), (3, 0, 4)]
|
||||
holes = []
|
||||
for y in range(10, 90, 10):
|
||||
for x in range(10, 90, 10):
|
||||
start = len(points)
|
||||
points.extend([(x, y), (x + 3, y), (x + 3, y + 3), (x, y + 3)])
|
||||
segments.extend(
|
||||
(start + i, start + (i + 1) % 4, len(segments) + i + 1)
|
||||
for i in range(4)
|
||||
)
|
||||
holes.append((x + 1.5, y + 1.5))
|
||||
with pytest.raises(ValueError, match="limit"):
|
||||
tangle.build_mesh(points, segments, holes, 0.001, max_vertices=300)
|
||||
p, f, _ = tangle.build_mesh(points, segments, holes, 40.0, max_vertices=5000)
|
||||
assert len(p) < 5000 and len(f) > 0
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ def test_cutout_depth_axis_is_adjusted_only_in_modifier(shape, gesture):
|
||||
@ Matrix.Diagonal((1.5, 0.75, 2.0, 1.0))
|
||||
)
|
||||
bpy.context.collection.objects.link(source)
|
||||
bpy.context.view_layer.update()
|
||||
|
||||
positive_x = _create_cutout_shape(
|
||||
source,
|
||||
@@ -56,11 +57,11 @@ def test_cutout_depth_axis_is_adjusted_only_in_modifier(shape, gesture):
|
||||
for result in (positive_x, negative_z):
|
||||
modifier = result.modifiers[0]
|
||||
mode = object_data.modifier_input_identifier(modifier.node_group, "Mode")
|
||||
assert modifier[mode] == (1 if gesture == "POLYLINE" else 0)
|
||||
assert modifier_value(modifier, mode) == (1 if gesture == "POLYLINE" else 0)
|
||||
thickness = object_data.modifier_input_identifier(
|
||||
modifier.node_group, "Thickness", subtype="DISTANCE",
|
||||
)
|
||||
assert modifier[thickness] == (0.5 if shape == "SOLID" and gesture == "POLYLINE" else 0.0)
|
||||
assert modifier_value(modifier, thickness) == (0.5 if shape == "SOLID" and gesture == "POLYLINE" else 0.0)
|
||||
local = np.asarray([tuple(vertex.co) for vertex in result.data.vertices])
|
||||
assert np.allclose(local[:, 2], 0.0, atol=1e-8)
|
||||
assert np.ptp(local[:, 0]) > 0.0
|
||||
@@ -70,7 +71,7 @@ def test_cutout_depth_axis_is_adjusted_only_in_modifier(shape, gesture):
|
||||
assert np.allclose(positive_x.matrix_world, source.matrix_world)
|
||||
modifier = positive_x.modifiers[0]
|
||||
axis = object_data.modifier_input_identifier(modifier.node_group, "Depth Axis")
|
||||
assert modifier[axis] == 0
|
||||
assert modifier_value(modifier, axis) == 0
|
||||
object_data.set_modifier_input(modifier, axis, 1)
|
||||
positive_x.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
@@ -261,9 +262,9 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
assert cutout_shape.BALLOON_ATTRIBUTE_NAME in result.data.attributes
|
||||
modifier = result.modifiers[0]
|
||||
assert modifier.node_group.name == cutout_shape.CUTOUT_NODE_GROUP_NAMES["DEPTH_SOLID"]
|
||||
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Mode")] == (1 if gesture == "POLYLINE" else 0)
|
||||
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE")] == 0.5
|
||||
assert modifier[object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")] == 1.0
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Mode")) == (1 if gesture == "POLYLINE" else 0)
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE")) == 0.5
|
||||
assert modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="NONE")) == 1.0
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(modifier.node_group, "Thickness", subtype="DISTANCE"),
|
||||
@@ -274,14 +275,12 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
modifier[object_data.modifier_input_identifier(modifier.node_group, "Cleanup Threshold")],
|
||||
modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Cleanup Threshold")),
|
||||
expected_cleanup,
|
||||
)
|
||||
assert np.isclose(modifier[object_data.modifier_input_identifier(modifier.node_group, "Smooth Weight")], 1.0)
|
||||
assert np.isclose(modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Smooth Weight")), 1.0)
|
||||
assert (
|
||||
modifier[
|
||||
object_data.modifier_input_identifier(modifier.node_group, "Smooth")
|
||||
]
|
||||
modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Smooth"))
|
||||
== 0
|
||||
)
|
||||
for name, value in (("Mode", 1), ("Front Inflation", 0.0)):
|
||||
@@ -290,7 +289,7 @@ def test_depth_surface_protected_smooth_evaluates(gesture):
|
||||
modifier.node_group,
|
||||
"Reference Depth",
|
||||
)
|
||||
assert modifier[reference_id] == 8.0
|
||||
assert modifier_value(modifier, reference_id) == 8.0
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
object_data.modifier_input_identifier(modifier.node_group, "Smooth Weight"),
|
||||
@@ -639,7 +638,7 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
modifier.node_group,
|
||||
"Depth Image",
|
||||
)
|
||||
assert isinstance(modifier[depth_image_id], bpy.types.Image)
|
||||
assert isinstance(modifier_value(modifier, depth_image_id), bpy.types.Image)
|
||||
expected_center = np.zeros(2, dtype=np.float64)
|
||||
crop_center = Vector(
|
||||
(
|
||||
@@ -659,18 +658,18 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
modifier.node_group,
|
||||
"Reference Depth",
|
||||
)
|
||||
assert not np.isclose(modifier[depth_reference_id], 8.0)
|
||||
assert not np.isclose(modifier_value(modifier, depth_reference_id), 8.0)
|
||||
depth_direction_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Depth Direction",
|
||||
)
|
||||
direction = np.asarray(modifier[depth_direction_id], dtype=np.float64)
|
||||
direction = np.asarray(modifier_value(modifier, depth_direction_id), dtype=np.float64)
|
||||
assert not np.allclose(direction, (0.0, 0.0, 1.0))
|
||||
uniform_scale_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Uniform Scale",
|
||||
)
|
||||
assert modifier[uniform_scale_id] > 0.0
|
||||
assert modifier_value(modifier, uniform_scale_id) > 0.0
|
||||
thickness_id = object_data.modifier_input_identifier(
|
||||
modifier.node_group,
|
||||
"Thickness",
|
||||
@@ -679,7 +678,7 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
modifier.node_group,
|
||||
"Smooth Weight",
|
||||
)
|
||||
assert modifier[smooth_id] == 1.0
|
||||
assert modifier_value(modifier, smooth_id) == 1.0
|
||||
object_data.set_modifier_input(modifier, smooth_id, 0.0)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
@@ -707,7 +706,7 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier[depth_reference_id] + 0.2,
|
||||
modifier_value(modifier, depth_reference_id) + 0.2,
|
||||
)
|
||||
result.update_tag(refresh={"DATA"})
|
||||
bpy.context.view_layer.update()
|
||||
@@ -729,7 +728,7 @@ def test_depth_balloon_uses_metric_plane_inputs():
|
||||
object_data.set_modifier_input(
|
||||
modifier,
|
||||
depth_reference_id,
|
||||
modifier[depth_reference_id] + 0.15,
|
||||
modifier_value(modifier, depth_reference_id) + 0.15,
|
||||
)
|
||||
object_data.set_modifier_input(modifier, depth_direction_id, (0.2, 0.1, 1.0))
|
||||
result.update_tag(refresh={"DATA"})
|
||||
@@ -889,17 +888,15 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
cutout_geometry.world_plane_metric(source.matrix_world)[0],
|
||||
)
|
||||
assert np.isclose(
|
||||
modifier[object_data.modifier_input_identifier(modifier.node_group, "Cleanup Threshold")],
|
||||
modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Cleanup Threshold")),
|
||||
expected_cleanup,
|
||||
)
|
||||
assert (
|
||||
modifier[object_data.modifier_input_identifier(modifier.node_group, "Smooth Weight")]
|
||||
modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Smooth Weight"))
|
||||
== 1.0
|
||||
)
|
||||
assert (
|
||||
modifier[
|
||||
object_data.modifier_input_identifier(modifier.node_group, "Smooth")
|
||||
]
|
||||
modifier_value(modifier, object_data.modifier_input_identifier(modifier.node_group, "Smooth"))
|
||||
== 0
|
||||
)
|
||||
assert len(result.data.vertices) > 4
|
||||
@@ -931,3 +928,15 @@ def test_local_cutout_builds_from_blender_image_pixels_without_a_job():
|
||||
assert np.allclose(
|
||||
copied[:, :, 3], np.flipud(rgba[top:bottom, left:right, 3])
|
||||
)
|
||||
|
||||
|
||||
def modifier_value(modifier, identifier):
|
||||
"""Read modifier values through the runtime's socket API."""
|
||||
inputs = object_data._modifier_input_slots(modifier)
|
||||
if inputs is None:
|
||||
return modifier[identifier]
|
||||
slot = getattr(inputs, identifier)
|
||||
prop = slot.bl_rna.properties["value"]
|
||||
if prop.type == "ENUM":
|
||||
return list(prop.enum_items.keys()).index(slot.value)
|
||||
return slot.value
|
||||
|
||||
Reference in New Issue
Block a user