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"}