Add cutout profile and teddy balloon height calculations

- Implemented cotangent Poisson field and Poisson balloon profile in cutout_profile.py.
- Added teddy balloon heights calculation and related functions in cutout_teddy.py.
- Introduced interaction utilities for image editing in interaction.py.
- Developed depth normalization and analysis functions in cutout_depth.py.
- Created a validation script for cutout assets in validate_cutout_asset.py.
This commit is contained in:
2026-08-27 22:46:57 +08:00
parent 6c8358f92f
commit 7d39fe8c5e
40 changed files with 3058 additions and 3896 deletions

View File

@@ -0,0 +1,165 @@
from pathlib import Path
import sys
import bpy
PROJECT_ROOT = Path(__file__).parents[1]
sys.path.insert(0, str(PROJECT_ROOT / "src"))
sys.path.insert(
0,
str(next((PROJECT_ROOT / "src" / "anyimage" / "wheels").glob("blendjob-*.whl"))),
)
from anyimage.operators import mesh
from anyimage.operators.material import modifier_input_identifier, set_modifier_input
def evaluated_mesh(obj):
evaluated = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
result = evaluated.to_mesh()
return evaluated, result
def grid_object(name):
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):
a = z * 3 + y
faces.extend(((a, a + 4, a + 1), (a, a + 3, a + 4)))
data = bpy.data.meshes.new(name)
data.from_pydata(vertices, (), faces)
obj = bpy.data.objects.new(name, data)
bpy.context.scene.collection.objects.link(obj)
return obj
def add_modifier(obj, shape, thickness):
group = mesh._mesh_node_group(shape)
modifier = obj.modifiers.new(group.name, "NODES")
modifier.node_group = group
set_modifier_input(
modifier,
modifier_input_identifier(group, "Cleanup"),
0.0,
)
set_modifier_input(
modifier,
modifier_input_identifier(group, "Thickness"),
thickness,
)
return modifier
def main():
bpy.ops.wm.read_factory_settings(use_empty=True)
groups = {
shape: mesh._mesh_node_group(shape)
for shape in (
mesh.CUTOUT,
mesh.BALLOON,
mesh.DEPTH_BALLOON,
mesh.DEPTH_SURFACE,
)
}
assert [groups[shape].name for shape in groups] == [
"O Image Surface",
"O Image Balloon",
"O Image Depth Balloon",
"O Image Depth Surface",
]
repeat_counts = {
shape: sum(
node.bl_idname == "GeometryNodeRepeatInput"
for node in group.nodes
)
for shape, group in groups.items()
}
assert repeat_counts == {
mesh.CUTOUT: 1,
mesh.BALLOON: 0,
mesh.DEPTH_BALLOON: 0,
mesh.DEPTH_SURFACE: 2,
}
for group in groups.values():
assert group.is_modifier
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
)
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:]:
assert not (
abs(node.location.x - other.location.x) < 150.0
and abs(node.location.y - other.location.y) < 100.0
)
for group_input in (
node for node in group_nodes if node.bl_idname == "NodeGroupInput"
):
assert all(
output.is_linked or output.hide
for output in group_input.outputs
if output.name
)
for item in group.interface.items_tree:
if item.item_type == "SOCKET" and item.in_out == "INPUT":
assert item.structure_type == "SINGLE"
surface = grid_object("Surface Validation")
add_modifier(surface, mesh.CUTOUT, 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
]
assert len(side_faces) == 24
finally:
evaluated.to_mesh_clear()
balloon = grid_object("Balloon Validation")
profile = balloon.data.attributes.new(mesh.BALLOON_ATTRIBUTE, "FLOAT", "POINT")
profile.data.foreach_set(
"value",
(0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0),
)
modifier = add_modifier(balloon, mesh.BALLOON, 1.0)
evaluated, result = evaluated_mesh(balloon)
try:
initial_faces = len(result.polygons)
initial_extent = max(abs(vertex.co.x) for vertex in result.vertices)
finally:
evaluated.to_mesh_clear()
set_modifier_input(
modifier,
modifier_input_identifier(modifier.node_group, "Thickness"),
2.0,
)
balloon.update_tag(refresh={"DATA"})
bpy.context.view_layer.update()
evaluated, result = evaluated_mesh(balloon)
try:
assert len(result.polygons) == initial_faces
extent = max(abs(vertex.co.x) for vertex in result.vertices)
assert abs(extent - initial_extent * 2.0) < 1e-5
finally:
evaluated.to_mesh_clear()
print(
f"Validated Blender {bpy.app.version_string}: "
f"{', '.join(group.name for group in groups.values())}"
)
if __name__ == "__main__":
main()