- 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.
229 lines
6.5 KiB
Python
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"}
|