From 1d894aa1a75daaecf40e66511841931b309c1b94 Mon Sep 17 00:00:00 2001 From: Nika Kutsniashvili Date: Thu, 6 Jun 2024 10:55:15 +1000 Subject: [PATCH] Add Convert Image Empty to Mesh Plane operator Since import images as mesh planes operator was added recently in core Blender, it is now easy to also support converting image empties to mesh planes by reusing the same code. This results in a fast workflow where you can use Blender's drag & drop & align feature for reference images, and quickly turn them into meshes without registering second file handler and clicking anything before import is finished. Ref !122546 --- .../startup/bl_operators/image_as_planes.py | 647 +++++++++++------- scripts/startup/bl_ui/space_view3d.py | 19 +- 2 files changed, 418 insertions(+), 248 deletions(-) diff --git a/scripts/startup/bl_operators/image_as_planes.py b/scripts/startup/bl_operators/image_as_planes.py index f9efb4914b5..563b7f9e023 100644 --- a/scripts/startup/bl_operators/image_as_planes.py +++ b/scripts/startup/bl_operators/image_as_planes.py @@ -237,9 +237,270 @@ def center_in_camera(camera, ob, axis=(1, 1)): ob.location = location + offset +def get_ref_object_space_coord(ob): + size = ob.empty_display_size + x, y = ob.empty_image_offset + img = ob.data + + res_x, res_y = img.size + scaling = 1.0 / max(res_x, res_y) + + corners = [ + (0.0, 0.0), + (res_x, 0.0), + (0.0, res_y), + (res_x, res_y), + ] + + obj_space_corners = [] + for co in corners: + nco_x = ((co[0] + (x * res_x)) * size) * scaling + nco_y = ((co[1] + (y * res_y)) * size) * scaling + obj_space_corners.append((nco_x, nco_y, 0)) + return obj_space_corners + + # ----------------------------------------------------------------------------- # Cycles/EEVEE utils +class MaterialProperties_MixIn: + shader: EnumProperty( + name="Shader", + items=( + ('PRINCIPLED', "Principled", "Principled shader"), + ('SHADELESS', "Shadeless", "Only visible to camera and reflections"), + ('EMISSION', "Emission", "Emission shader"), + ), + default='PRINCIPLED', + description="Node shader to use", + ) + + emit_strength: FloatProperty( + name="Emission Strength", + min=0.0, + default=1.0, + soft_max=10.0, + step=100, + description="Strength of emission", + ) + + use_transparency: BoolProperty( + name="Use Alpha", + default=True, + description="Use alpha channel for transparency", + ) + + blend_method: EnumProperty( + name="Blend Mode", + items=( + ('BLEND', "Blend", "Render polygon transparent, depending on alpha channel of the texture"), + ('CLIP', "Clip", "Use the alpha threshold to clip the visibility (binary visibility)"), + ('HASHED', "Hashed", "Use noise to dither the binary visibility (works well with multi-samples)"), + ('OPAQUE', "Opaque", "Render surface without transparency"), + ), + default='BLEND', + description="Blend Mode for Transparent Faces", + translation_context=i18n_contexts.id_material, + ) + + shadow_method: EnumProperty( + name="Shadow Mode", + items=( + ('CLIP', "Clip", "Use the alpha threshold to clip the visibility (binary visibility)"), + ('HASHED', "Hashed", "Use noise to dither the binary visibility (works well with multi-samples)"), + ('OPAQUE', "Opaque", "Material will cast shadows without transparency"), + ('NONE', "None", "Material will cast no shadow"), + ), + default='CLIP', + description="Shadow mapping method", + translation_context=i18n_contexts.id_material, + ) + + use_backface_culling: BoolProperty( + name="Backface Culling", + default=False, + description="Use backface culling to hide the back side of faces", + ) + + show_transparent_back: BoolProperty( + name="Show Backface", + default=True, + description="Render multiple transparent layers (may introduce transparency sorting problems)", + ) + + overwrite_material: BoolProperty( + name="Overwrite Material", + default=True, + description="Overwrite existing material with the same name", + ) + + def draw_material_config(self, context): + # --- Material / Rendering Properties --- # + layout = self.layout + + header, body = layout.panel("import_image_plane_material", default_closed=False) + header.label(text="Material") + if body: + body.prop(self, 'shader') + if self.shader == 'EMISSION': + body.prop(self, "emit_strength") + + body.prop(self, 'blend_method') + + body.prop(self, 'shadow_method') + if self.blend_method == 'BLEND': + body.prop(self, "show_transparent_back") + + body.prop(self, "use_backface_culling") + + engine = context.scene.render.engine + if engine not in ('CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'): + body.label(text=tip_("{:s} is not supported").format(engine), icon='ERROR') + + body.prop(self, "overwrite_material") + + +class TextureProperties_MixIn: + interpolation: EnumProperty( + name="Interpolation", + items=( + ('Linear', "Linear", "Linear interpolation"), + ('Closest', "Closest", "No interpolation (sample closest texel)"), + ('Cubic', "Cubic", "Cubic interpolation"), + ('Smart', "Smart", "Bicubic when magnifying, else bilinear (OSL only)"), + ), + default='Linear', + description="Texture interpolation", + ) + + extension: EnumProperty( + name="Extension", + items=( + ('CLIP', "Clip", "Clip to image size and set exterior pixels as transparent"), + ('EXTEND', "Extend", "Extend by repeating edge pixels of the image"), + ('REPEAT', "Repeat", "Cause the image to repeat horizontally and vertically"), + ), + default='CLIP', + description="How the image is extrapolated past its original bounds", + ) + + t = bpy.types.Image.bl_rna.properties["alpha_mode"] + alpha_mode: EnumProperty( + name=t.name, + items=tuple((e.identifier, e.name, e.description) for e in t.enum_items), + default=t.default, + description=t.description, + ) + + t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"] + use_auto_refresh: BoolProperty( + name=t.name, + default=True, + description=t.description, + ) + + relative: BoolProperty( + name="Relative Paths", + default=True, + description="Use relative file paths", + ) + + def draw_texture_config(self, context): + # --- Texture Properties --- # + layout = self.layout + + header, body = layout.panel("import_image_plane_texture", default_closed=False) + header.label(text="Texture") + if body: + body.prop(self, 'interpolation') + body.prop(self, 'extension') + + row = body.row(align=False, heading="Alpha") + row.prop(self, "use_transparency", text="") + sub = row.row(align=True) + sub.active = self.use_transparency + sub.prop(self, "alpha_mode", text="") + + body.prop(self, "use_auto_refresh") + + +def apply_texture_options(self, texture, img_spec): + # Shared by both Cycles and Blender Internal. + image_user = texture.image_user + image_user.use_auto_refresh = self.use_auto_refresh + image_user.frame_start = img_spec.frame_start + image_user.frame_offset = img_spec.frame_offset + image_user.frame_duration = img_spec.frame_duration + + # Image sequences need auto refresh to display reliably. + if img_spec.image.source == 'SEQUENCE': + image_user.use_auto_refresh = True + + +def create_cycles_texnode(self, node_tree, img_spec): + tex_image = node_tree.nodes.new('ShaderNodeTexImage') + tex_image.image = img_spec.image + tex_image.show_texture = True + tex_image.interpolation = self.interpolation + tex_image.extension = self.extension + apply_texture_options(self, tex_image, img_spec) + return tex_image + + +def create_cycles_material(self, context, img_spec, name): + material = None + if self.overwrite_material: + material = bpy.data.materials.get((name, None)) + if material is None: + material = bpy.data.materials.new(name=name) + + material.use_nodes = True + + material.blend_method = self.blend_method + material.shadow_method = self.shadow_method + + material.use_backface_culling = self.use_backface_culling + material.use_transparency_overlap = self.show_transparent_back + + node_tree = material.node_tree + out_node = clean_node_tree(node_tree) + + tex_image = create_cycles_texnode(self, node_tree, img_spec) + + if self.shader == 'PRINCIPLED': + core_shader = node_tree.nodes.new('ShaderNodeBsdfPrincipled') + elif self.shader == 'SHADELESS': + core_shader = get_shadeless_node(node_tree) + elif self.shader == 'EMISSION': + core_shader = node_tree.nodes.new('ShaderNodeBsdfPrincipled') + core_shader.inputs["Emission Strength"].default_value = self.emit_strength + core_shader.inputs["Base Color"].default_value = (0.0, 0.0, 0.0, 1.0) + core_shader.inputs["Specular IOR Level"].default_value = 0.0 + + # Connect color from texture. + if self.shader in {'PRINCIPLED', 'SHADELESS'}: + node_tree.links.new(core_shader.inputs[0], tex_image.outputs["Color"]) + elif self.shader == 'EMISSION': + node_tree.links.new(core_shader.inputs["Emission Color"], tex_image.outputs["Color"]) + + if self.use_transparency: + if self.shader in {'PRINCIPLED', 'EMISSION'}: + node_tree.links.new(core_shader.inputs["Alpha"], tex_image.outputs["Alpha"]) + else: + bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent') + + mix_shader = node_tree.nodes.new('ShaderNodeMixShader') + node_tree.links.new(mix_shader.inputs["Fac"], tex_image.outputs["Alpha"]) + node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs["BSDF"]) + node_tree.links.new(mix_shader.inputs[2], core_shader.outputs[0]) + core_shader = mix_shader + + node_tree.links.new(out_node.inputs["Surface"], core_shader.outputs[0]) + + auto_align_nodes(node_tree) + return material + + def get_input_nodes(node, links): """Get nodes that are a inputs to the given node""" # Get all links going to node. @@ -384,7 +645,8 @@ def get_shadeless_node(dest_node_tree): # ----------------------------------------------------------------------------- # Operator -class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): +class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, MaterialProperties_MixIn, + TextureProperties_MixIn, Operator): """Create mesh plane(s) from image files with the appropriate aspect ratio""" bl_idname = "image.import_as_mesh_planes" @@ -548,124 +810,6 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): description="Number of pixels per inch or Blender Unit", ) - # ------------------------------ - # Properties - Material / Shader - shader: EnumProperty( - name="Shader", - items=( - ('PRINCIPLED', "Principled", "Principled Shader"), - ('SHADELESS', "Shadeless", "Only visible to camera and reflections"), - ('EMISSION', "Emit", "Emission Shader"), - ), - default='PRINCIPLED', - description="Node shader to use", - ) - - emit_strength: FloatProperty( - name="Emission Strength", - min=0.0, - default=1.0, - soft_max=10.0, - step=100, - description="Strength of emission", - ) - - use_transparency: BoolProperty( - name="Use Alpha", - default=True, - description="Use alpha channel for transparency", - ) - - blend_method: EnumProperty( - name="Blend Mode", - items=( - ('BLEND', "Blend", "Render polygon transparent, depending on alpha channel of the texture"), - ('CLIP', "Clip", "Use the alpha threshold to clip the visibility (binary visibility)"), - ('HASHED', "Hashed", "Use noise to dither the binary visibility (works well with multi-samples)"), - ('OPAQUE', "Opaque", "Render surface without transparency"), - ), - default='BLEND', - description="Blend Mode for Transparent Faces", - translation_context=i18n_contexts.id_material, - ) - - shadow_method: EnumProperty( - name="Shadow Mode", - items=( - ('CLIP', "Clip", "Use the alpha threshold to clip the visibility (binary visibility)"), - ('HASHED', "Hashed", "Use noise to dither the binary visibility (works well with multi-samples)"), - ('OPAQUE', "Opaque", "Material will cast shadows without transparency"), - ('NONE', "None", "Material will cast no shadow"), - ), - default='CLIP', - description="Shadow mapping method", - translation_context=i18n_contexts.id_material, - ) - - use_backface_culling: BoolProperty( - name="Backface Culling", - default=False, - description="Use backface culling to hide the back side of faces", - ) - - show_transparent_back: BoolProperty( - name="Show Backface", - default=True, - description="Render multiple transparent layers (may introduce transparency sorting problems)", - ) - - overwrite_material: BoolProperty( - name="Overwrite Material", - default=True, - description="Overwrite existing material with the same name", - ) - - # ------------------ - # Properties - Image - interpolation: EnumProperty( - name="Interpolation", - items=( - ('Linear', "Linear", "Linear interpolation"), - ('Closest', "Closest", "No interpolation (sample closest texel)"), - ('Cubic', "Cubic", "Cubic interpolation"), - ('Smart', "Smart", "Bicubic when magnifying, else bilinear (OSL only)"), - ), - default='Linear', - description="Texture interpolation", - ) - - extension: EnumProperty( - name="Extension", - items=( - ('CLIP', "Clip", "Clip to image size and set exterior pixels as transparent"), - ('EXTEND', "Extend", "Extend by repeating edge pixels of the image"), - ('REPEAT', "Repeat", "Cause the image to repeat horizontally and vertically"), - ), - default='CLIP', - description="How the image is extrapolated past its original bounds", - ) - - t = bpy.types.Image.bl_rna.properties["alpha_mode"] - alpha_mode: EnumProperty( - name=t.name, - items=tuple((e.identifier, e.name, e.description) for e in t.enum_items), - default=t.default, - description=t.description, - ) - - t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"] - use_auto_refresh: BoolProperty( - name=t.name, - default=True, - description=t.description, - ) - - relative: BoolProperty( - name="Relative Paths", - default=True, - description="Use relative file paths", - ) - # ------- # Draw UI @@ -682,49 +826,6 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): body.prop(self, "force_reload") body.prop(self, "image_sequence") - def draw_material_config(self, context): - # --- Material / Rendering Properties --- # - layout = self.layout - - header, body = layout.panel("import_image_plane_material", default_closed=False) - header.label(text="Material") - if body: - body.prop(self, 'shader') - if self.shader == 'EMISSION': - body.prop(self, "emit_strength") - - body.prop(self, 'blend_method') - - body.prop(self, 'shadow_method') - if self.blend_method == 'BLEND': - body.prop(self, "show_transparent_back") - - body.prop(self, "use_backface_culling") - - engine = context.scene.render.engine - if engine not in ('CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'): - body.label(text=tip_("{:s} is not supported").format(engine), icon='ERROR') - - body.prop(self, "overwrite_material") - - def draw_texture_config(self, context): - # --- Texture Properties --- # - layout = self.layout - - header, body = layout.panel("import_image_plane_texture", default_closed=False) - header.label(text="Texture") - if body: - body.prop(self, 'interpolation') - body.prop(self, 'extension') - - row = body.row(align=False, heading="Alpha") - row.prop(self, "use_transparency", text="") - sub = row.row(align=True) - sub.active = self.use_transparency - sub.prop(self, "alpha_mode", text="") - - body.prop(self, "use_auto_refresh") - def draw_spatial_config(self, _context): # --- Spatial Properties: Position, Size and Orientation --- # layout = self.layout @@ -758,8 +859,8 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): layout.use_property_split = True self.draw_import_config(context) - self.draw_material_config(context) - self.draw_texture_config(context) + MaterialProperties_MixIn.draw_material_config(self, context) + TextureProperties_MixIn.draw_texture_config(self, context) self.draw_spatial_config(context) # ------------------------------------------------------------------------- @@ -845,7 +946,7 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): # Configure material. # TODO: check `context.scene.render.engine` and support other engines. - material = self.create_cycles_material(img_spec, name) + material = create_cycles_material(self, context, img_spec, name) # Create and position plane object. plane = self.create_image_plane(context, name, img_spec) @@ -867,18 +968,6 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): except ValueError: pass - def apply_texture_options(self, texture, img_spec): - # Shared by both Cycles and Blender Internal. - image_user = texture.image_user - image_user.use_auto_refresh = self.use_auto_refresh - image_user.frame_start = img_spec.frame_start - image_user.frame_offset = img_spec.frame_offset - image_user.frame_duration = img_spec.frame_duration - - # Image sequences need auto refresh to display reliably. - if img_spec.image.source == 'SEQUENCE': - image_user.use_auto_refresh = True - def apply_material_options(self, material, slot): shader = self.shader @@ -899,70 +988,6 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): material.use_transparent_shadows = (shader == 'DIFFUSE') material.emit = self.emit_strength if shader == 'EMISSION' else 0.0 - # ------------------------------------------------------------------------- - # Cycles/EEVEE - def create_cycles_texnode(self, node_tree, img_spec): - tex_image = node_tree.nodes.new('ShaderNodeTexImage') - tex_image.image = img_spec.image - tex_image.show_texture = True - tex_image.interpolation = self.interpolation - tex_image.extension = self.extension - self.apply_texture_options(tex_image, img_spec) - return tex_image - - def create_cycles_material(self, img_spec, name): - material = None - if self.overwrite_material: - material = bpy.data.materials.get((name, None)) - if material is None: - material = bpy.data.materials.new(name=name) - - material.use_nodes = True - - material.blend_method = self.blend_method - material.shadow_method = self.shadow_method - - material.use_backface_culling = self.use_backface_culling - material.use_transparency_overlap = self.show_transparent_back - - node_tree = material.node_tree - out_node = clean_node_tree(node_tree) - - tex_image = self.create_cycles_texnode(node_tree, img_spec) - - if self.shader == 'PRINCIPLED': - core_shader = node_tree.nodes.new('ShaderNodeBsdfPrincipled') - elif self.shader == 'SHADELESS': - core_shader = get_shadeless_node(node_tree) - elif self.shader == 'EMISSION': - core_shader = node_tree.nodes.new('ShaderNodeBsdfPrincipled') - core_shader.inputs["Emission Strength"].default_value = self.emit_strength - core_shader.inputs["Base Color"].default_value = (0.0, 0.0, 0.0, 1.0) - core_shader.inputs["Specular IOR Level"].default_value = 0.0 - - # Connect color from texture. - if self.shader in {'PRINCIPLED', 'SHADELESS'}: - node_tree.links.new(core_shader.inputs[0], tex_image.outputs["Color"]) - elif self.shader == 'EMISSION': - node_tree.links.new(core_shader.inputs["Emission Color"], tex_image.outputs["Color"]) - - if self.use_transparency: - if self.shader in {'PRINCIPLED', 'EMISSION'}: - node_tree.links.new(core_shader.inputs["Alpha"], tex_image.outputs["Alpha"]) - else: - bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent') - - mix_shader = node_tree.nodes.new('ShaderNodeMixShader') - node_tree.links.new(mix_shader.inputs["Fac"], tex_image.outputs["Alpha"]) - node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs["BSDF"]) - node_tree.links.new(mix_shader.inputs[2], core_shader.outputs[0]) - core_shader = mix_shader - - node_tree.links.new(out_node.inputs["Surface"], core_shader.outputs[0]) - - auto_align_nodes(node_tree) - return material - # ------------------------------------------------------------------------- # Geometry Creation def create_image_plane(self, context, name, img_spec): @@ -1075,6 +1100,146 @@ class IMAGE_OT_import_as_mesh_planes(AddObjectHelper, ImportHelper, Operator): constraint.lock_axis = 'LOCK_Y' +class IMAGE_OT_convert_to_mesh_plane(MaterialProperties_MixIn, TextureProperties_MixIn, Operator): + """Convert selected reference images to textured mesh plane""" + bl_idname = "image.convert_to_mesh_plane" + bl_label = "Convert Image Empty to Mesh Plane" + bl_options = {'REGISTER', 'PRESET', 'UNDO'} + + name_from: EnumProperty( + name="Name After", + items=[ + ('OBJECT', "Source Object", "Name after object source with a suffix"), + ('IMAGE', "Source Image", "name from laoded image"), + ], + default='OBJECT', + description="Name for new mesh object and material" + ) + + delete_ref: BoolProperty( + name="Delete Reference Object", + default=True, + description="Delete empty image object once mesh plane is created" + ) + + @classmethod + def poll(cls, context): + ob = context.object + return ( + ob is not None and + ob.select_get() and + cls._is_object_empty_image(ob) + ) + + @staticmethod + def _is_object_empty_image(ob): + return ( + (ob.type == 'EMPTY') and + (ob.empty_display_type == 'IMAGE') and + (ob.data is not None) + ) + + def invoke(self, context, _event): + scene = context.scene + engine = scene.render.engine + + if engine not in COMPATIBLE_ENGINES: + self.report({'ERROR'}, tip_("Cannot generate materials for unknown {:s} render engine").format(engine)) + return {'CANCELLED'} + + if engine == 'BLENDER_WORKBENCH': + self.report( + {'WARNING'}, + tip_("Generating Cycles/EEVEE compatible material, but won't be visible with {:s} engine").format( + engine, + )) + + return context.window_manager.invoke_props_dialog(self) + + def execute(self, context): + scene = context.scene + + selected_objects = [ob for ob in context.selected_objects] + converted = 0 + + for ob in selected_objects: + if not self._is_object_empty_image(ob): + continue + + img = ob.data + img_user = ob.image_user + ob_name = ob.name + + # Give Name. + if self.name_from == 'IMAGE': + name = bpy.path.display_name(img.name, title_case=False) + else: + assert self.name_from == 'OBJECT' + name = ob.name + + # Create Mesh Plane. + obj_space_corners = get_ref_object_space_coord(ob) + + face = [(0, 1, 3, 2)] + mesh = bpy.data.meshes.new(name) + mesh.from_pydata(obj_space_corners, [], face) + plane = bpy.data.objects.new(name, mesh) + mesh.uv_layers.new(name="UVMap") + + # Link in the Same Collections. + users_collection = ob.users_collection + for collection in users_collection: + collection.objects.link(plane) + + # Assign Parent. + plane.parent = ob.parent + plane.matrix_local = ob.matrix_local + plane.matrix_parent_inverse = ob.matrix_parent_inverse + + # Create Material. + img_spec = ImageSpec( + img, + (img.size[0], img.size[1]), + img_user.frame_start, + img_user.frame_offset, + img_user.frame_duration, + ) + material = create_cycles_material(self, context, img_spec, name) + plane.data.materials.append(material) + + # Delete Empty. + if self.delete_ref: + for collection in users_collection: + collection.objects.unlink(ob) + mesh.name = ob_name + plane.name = ob_name + + plane.select_set(True) + converted += 1 + + if not converted: + self.report({'ERROR'}, "No images converted") + return {'CANCELLED'} + + self.report({'INFO'}, "{:d} image(s) converted to mesh plane(s)".format(converted)) + return {'FINISHED'} + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + + # General. + col = layout.column(align=False) + col.prop(self, "name_from") + col.prop(self, "delete_ref") + layout.separator() + + # Material. + MaterialProperties_MixIn.draw_material_config(self, context) + TextureProperties_MixIn.draw_texture_config(self, context) + + classes = ( IMAGE_OT_import_as_mesh_planes, + IMAGE_OT_convert_to_mesh_plane, ) diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index f4db5014393..71a086bf46d 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -3108,6 +3108,7 @@ class VIEW3D_MT_object_context_menu(Menu): layout.separator() if obj.empty_display_type == 'IMAGE': + layout.operator("image.convert_to_mesh_plane", text="Convert to Mesh Plane") layout.operator("gpencil.trace_image") layout.separator() @@ -3500,14 +3501,18 @@ class VIEW3D_MT_object_convert(Menu): layout = self.layout ob = context.active_object - if ob and ob.type == 'GPENCIL' and context.gpencil_data and not context.preferences.experimental.use_grease_pencil_version3: - layout.operator_enum("gpencil.convert", "type") - else: - layout.operator_enum("object.convert", "target") + if ob and ob.type != "EMPTY": + if (ob.type == 'GPENCIL' and context.gpencil_data + and not context.preferences.experimental.use_grease_pencil_version3): + layout.operator_enum("gpencil.convert", "type") + else: + layout.operator_enum("object.convert", "target") - # Potrace lib dependency. - if bpy.app.build_options.potrace: - layout.operator("gpencil.trace_image", icon='OUTLINER_OB_GREASEPENCIL') + else: + # Potrace lib dependency. + if bpy.app.build_options.potrace: + layout.operator("image.convert_to_mesh_plane", text="Convert to Mesh Plane", icon='MESH_PLANE') + layout.operator("gpencil.trace_image", icon='OUTLINER_OB_GREASEPENCIL') if ob and ob.type == 'CURVES': layout.operator("curves.convert_to_particle_system", text="Particle System")