Files
AnyImage/src/anyimage/operators/mesh_plane.py
nan 988332548e Add tests for clipboard image functionality and mesh plane asset creation
- Implemented a comprehensive test suite for the clipboard image module, covering various functionalities including operator registration, context handling, and image processing.
- Created a new mesh plane asset with customizable parameters and integrated it into the geometry node system.
- Developed a validation script to ensure the correct behavior of the mesh plane asset, including input handling and output verification.
2026-08-22 09:49:00 +08:00

229 lines
6.5 KiB
Python

import bpy
from .image import (
image_user_settings,
is_image_empty,
material_data_name,
plane_bounds,
)
from .material import (
create_material,
load_mesh_plane_node_group,
modifier_input_identifier,
set_modifier_input,
)
MESH_PLANE_NODE_GROUP_NAME = "O Mesh Plane"
def is_mesh_plane_object(candidate):
if candidate is None or getattr(candidate, "type", None) != "MESH":
return False
return any(
getattr(getattr(modifier, "node_group", None), "name", None)
== MESH_PLANE_NODE_GROUP_NAME
for modifier in getattr(candidate, "modifiers", ())
)
def flatten_matrix(matrix):
return tuple(
float(matrix[row][column]) for row in range(4) for column in range(4)
)
def restore_matrix(values):
from mathutils import Matrix
return Matrix(
tuple(
tuple(values[row * 4 + column] for column in range(4))
for row in range(4)
)
)
def create_mesh_plane_mesh(name, bounds):
x_min, x_max, y_min, y_max = bounds
if x_max <= x_min or y_max <= y_min:
raise ValueError("The plane has invalid dimensions")
mesh = bpy.data.meshes.new(name)
mesh.from_pydata(
(
(x_min, y_min, 0.0),
(x_max, y_min, 0.0),
(x_max, y_max, 0.0),
(x_min, y_max, 0.0),
),
(),
((0, 1, 2, 3),),
)
mesh.update()
uv_layer = mesh.uv_layers.new(name="UVMap")
for loop, uv in zip(
mesh.loops,
((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)),
):
uv_layer.data[loop.index].uv = uv
return mesh
def add_mesh_plane_processing(
plane_object,
material,
subdivisions=0,
thickness=0.0,
):
node_group = load_mesh_plane_node_group()
modifier = plane_object.modifiers.new(
name=MESH_PLANE_NODE_GROUP_NAME,
type="NODES",
)
modifier.node_group = node_group
if hasattr(modifier, "show_group_selector"):
modifier.show_group_selector = False
for name, value in (
("Subdivide", max(0, int(subdivisions))),
("Thickness", max(0.0, float(thickness))),
("Material", material),
):
identifier = modifier_input_identifier(node_group, name)
set_modifier_input(modifier, identifier, value)
return modifier
def create_mesh_plane_object(
context,
name,
bounds,
material,
matrix_world=None,
subdivisions=0,
thickness=0.0,
):
mesh = create_mesh_plane_mesh(name, bounds)
mesh.materials.append(material)
plane = bpy.data.objects.new(name, mesh)
context.collection.objects.link(plane)
if matrix_world is not None:
plane.matrix_world = matrix_world.copy()
add_mesh_plane_processing(
plane,
material,
subdivisions=subdivisions,
thickness=thickness,
)
return plane
def finish_mesh_plane(context, source_object, plane_object, keep_original):
source_name = source_object.name
for selected in context.selected_objects:
selected.select_set(False)
plane_object.select_set(True)
context.view_layer.objects.active = plane_object
if not keep_original:
bpy.data.objects.remove(source_object, do_unlink=True)
plane_object.name = source_name
return plane_object
def build_mesh_plane(
context,
source_object,
keep_original,
shadeless=False,
matrix_world=None,
):
frame_start, frame_offset, frame_duration, use_cyclic = image_user_settings(
source_object
)
material = create_material(
material_data_name(source_object),
source_object.data,
abs(float(source_object.scale.z)),
shadeless=shadeless,
frame_start=frame_start,
color_frame_offset=frame_offset,
color_frame_duration=frame_duration,
use_cyclic=use_cyclic,
scene=context.scene,
)
plane = create_mesh_plane_object(
context,
source_object.name,
plane_bounds(source_object),
material,
matrix_world=(
source_object.matrix_world if matrix_world is None else matrix_world
),
)
return finish_mesh_plane(context, source_object, plane, keep_original)
class ConvertToMeshPlane(bpy.types.Operator):
bl_idname = "anyimage.convert_to_mesh_plane"
bl_label = "Convert to Mesh Plane"
bl_description = "Convert an image to an editable mesh plane without AI depth"
bl_options = {"REGISTER", "UNDO"}
shadeless: bpy.props.BoolProperty(
name="Shadeless",
description="Display the source color without scene lighting",
default=False,
)
keep_original: bpy.props.BoolProperty(
name="Keep Original",
description="Keep the source Image Empty after conversion",
default=False,
)
source_object_name: bpy.props.StringProperty(options={"HIDDEN"})
source_matrix_world: bpy.props.FloatVectorProperty(
size=16,
default=(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
),
options={"HIDDEN"},
)
@classmethod
def poll(cls, context):
return is_image_empty(context.object) or is_mesh_plane_object(context.object)
def capture_source_state(self, source_object):
self.source_object_name = source_object.name
self.source_matrix_world = flatten_matrix(source_object.matrix_world)
def invoke(self, context, _event):
self.capture_source_state(context.object)
return self.execute(context)
def execute(self, context):
try:
source_object = context.object
if not self.source_object_name:
if not is_image_empty(source_object):
raise ValueError("Select an Image Empty to convert")
self.capture_source_state(source_object)
stored_source = bpy.data.objects.get(self.source_object_name)
if is_image_empty(stored_source):
source_object = stored_source
elif not is_image_empty(source_object):
raise ValueError("The source Image Empty is no longer available")
build_mesh_plane(
context,
source_object,
keep_original=self.keep_original,
shadeless=self.shadeless,
matrix_world=restore_matrix(self.source_matrix_world),
)
except (RuntimeError, ValueError) as error:
self.report({"ERROR"}, str(error))
return {"CANCELLED"}
return {"FINISHED"}