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.
This commit is contained in:
228
src/anyimage/operators/mesh_plane.py
Normal file
228
src/anyimage/operators/mesh_plane.py
Normal file
@@ -0,0 +1,228 @@
|
||||
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"}
|
||||
Reference in New Issue
Block a user