feat(cutout_tool): Implement cutout region to shape functionality

- Added interaction helpers for cutout region validation and shape menu interaction.
- Created main operator for converting image regions into cutout shapes, including AI artifact generation.
- Introduced object creation logic for cutout shapes, handling depth and normal textures.
- Defined cutout shape identifiers and node groups for various shape types.
- Implemented depth texture writing and prediction artifact generation for MoGe-2.
- Developed server jobs for generating cutout artifacts, depth planes, and refining image regions.
- Added region refinement logic to enhance gesture masks using BEN2.
This commit is contained in:
2026-08-31 11:36:27 +08:00
parent 08309f719d
commit 87d7721128
80 changed files with 4823 additions and 4297 deletions

View File

@@ -1,9 +1,8 @@
from pathlib import Path
import sys
from pathlib import Path
import bpy
PROJECT_ROOT = Path(__file__).parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
@@ -11,12 +10,12 @@ sys.path.insert(
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators.cutout_tool import node_assets as cutout_nodes
from anyimage.operators.cutout_tool import shape_types as shapes
from anyimage.operators.common.mesh_result import (
modifier_input_identifier,
set_modifier_input,
)
from anyimage.operators.cutout_tool import object as cutout_objects
from anyimage.operators.cutout_tool import shape as shapes
def evaluated_mesh(obj):
@@ -34,11 +33,7 @@ def input_names(group):
def grid_object(name):
vertices = [
(0.0, float(y), float(z))
for z in range(3)
for y in range(3)
]
vertices = [(0.0, float(y), float(z)) for z in range(3) for y in range(3)]
faces = []
for z in range(2):
for y in range(2):
@@ -56,7 +51,7 @@ def grid_object(name):
def add_modifier(obj, shape, thickness):
group = cutout_nodes.cutout_node_group(shape)
group = cutout_objects.load_cutout_node_group()
modifier = obj.modifiers.new(group.name, "NODES")
modifier.node_group = group
for name, value in (
@@ -76,9 +71,9 @@ def add_modifier(obj, shape, thickness):
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
wrapper = cutout_nodes.cutout_node_group("CUTOUT")
wrapper = cutout_objects.load_cutout_node_group()
assert all(
cutout_nodes.cutout_node_group(shape) == wrapper
cutout_objects.load_cutout_node_group() == wrapper
for shape in shapes.CUTOUT_SHAPES
)
assert wrapper.name == shapes.CUTOUT_NODE_GROUP_NAME
@@ -90,8 +85,7 @@ def main():
assert all(not group.is_modifier for group in groups.values())
menu_switch = next(
node for node in wrapper.nodes
if node.bl_idname == "GeometryNodeMenuSwitch"
node for node in wrapper.nodes if node.bl_idname == "GeometryNodeMenuSwitch"
)
assert [item.name for item in menu_switch.enum_items] == list(
shapes.CUTOUT_SHAPE_LABELS.values()
@@ -131,7 +125,7 @@ def main():
"Depth Image",
]
expected_inputs = {
"CUTOUT": ["Geometry", "Thickness"],
"SURFACE": ["Geometry", "Thickness"],
"BALLOON": ["Geometry", "Thickness"],
"DEPTH_BALLOON": [
"Geometry",
@@ -162,9 +156,7 @@ def main():
if link.to_node.bl_idname != "GeometryNodeGroup":
continue
shape = next(
shape
for shape, group in groups.items()
if group == link.to_node.node_tree
shape for shape, group in groups.items() if group == link.to_node.node_tree
)
dependencies.setdefault(link.from_socket.name, set()).add(shape)
assert dependencies == {
@@ -178,41 +170,30 @@ def main():
}
repeat_counts = {
shape: sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in group.nodes
)
shape: sum(node.bl_idname == "GeometryNodeRepeatInput" for node in group.nodes)
for shape, group in groups.items()
}
assert repeat_counts == {
"CUTOUT": 1,
"SURFACE": 1,
"BALLOON": 0,
"DEPTH_BALLOON": 0,
"DEPTH_SURFACE": 1,
}
assert sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in wrapper.nodes
) == 1
assert sum(
node.bl_idname == "GeometryNodeInputMeshIsland"
for node in wrapper.nodes
) == 1
assert (
sum(node.bl_idname == "GeometryNodeRepeatInput" for node in wrapper.nodes) == 1
)
assert (
sum(node.bl_idname == "GeometryNodeInputMeshIsland" for node in wrapper.nodes)
== 1
)
assert not any(
node.bl_idname == "GeometryNodeAttributeStatistic"
for node in wrapper.nodes
node.bl_idname == "GeometryNodeAttributeStatistic" for node in wrapper.nodes
)
assert all(
not any(
node.bl_idname == "GeometryNodeInputMeshIsland"
for node in group.nodes
)
not any(node.bl_idname == "GeometryNodeInputMeshIsland" for node in group.nodes)
for group in groups.values()
)
assert any(
node.bl_idname == "GeometryNodeBlurAttribute"
for node in wrapper.nodes
)
assert any(node.bl_idname == "GeometryNodeBlurAttribute" for node in wrapper.nodes)
smooth = next(
item
for item in wrapper.interface.items_tree
@@ -244,23 +225,19 @@ def main():
assert iterations.min_value == 0
assert getattr(iterations.parent, "name", "") == ""
assert all(
not any(
node.bl_idname == "GeometryNodeBlurAttribute"
for node in group.nodes
)
not any(node.bl_idname == "GeometryNodeBlurAttribute" for node in group.nodes)
for group in groups.values()
)
for group in (wrapper, *groups.values()):
assert all(not node.label for node in group.nodes)
assert all(
link.from_node.location.x <= link.to_node.location.x
for link in group.links
link.from_node.location.x <= link.to_node.location.x for link in group.links
)
group_nodes = list(group.nodes)
assert len({tuple(node.location) for node in group_nodes}) == len(group_nodes)
for index, node in enumerate(group_nodes):
for other in group_nodes[index + 1:]:
for other in group_nodes[index + 1 :]:
assert not (
abs(node.location.x - other.location.x) < 150.0
and abs(node.location.y - other.location.y) < 100.0
@@ -277,14 +254,13 @@ def main():
if item.item_type == "SOCKET" and item.in_out == "INPUT":
assert item.structure_type == "SINGLE"
surface = grid_object("Surface Validation")
add_modifier(surface, "CUTOUT", 2.4)
add_modifier(surface, "SURFACE", 2.4)
evaluated, result = evaluated_mesh(surface)
try:
assert len(result.vertices) == 34
assert len(result.polygons) == 40
side_faces = [
polygon for polygon in result.polygons
if abs(polygon.normal.x) < 0.1
polygon for polygon in result.polygons if abs(polygon.normal.x) < 0.1
]
assert len(side_faces) == 24
finally:
@@ -415,14 +391,12 @@ def main():
try:
coordinates = [tuple(vertex.co) for vertex in result.vertices]
assert all(
all(abs(actual - expected) < 1e-5 for actual, expected in zip(point, target))
all(
abs(actual - expected) < 1e-5 for actual, expected in zip(point, target)
)
for point, target in zip(
coordinates,
(
(y * 0.25, y * 0.5, z * 0.5)
for z in range(3)
for y in range(3)
),
((y * 0.25, y * 0.5, z * 0.5) for z in range(3) for y in range(3)),
)
)
finally: