1# ##### BEGIN GPL LICENSE BLOCK ##### 2# 3# This program is free software; you can redistribute it and/or 4# modify it under the terms of the GNU General Public License 5# as published by the Free Software Foundation; either version 2 6# of the License, or (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software Foundation, 15# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16# 17# ##### END GPL LICENSE BLOCK ##### 18 19# <pep8-80 compliant> 20 21bl_info = { 22 "name": "UV Layout", 23 "author": "Campbell Barton, Matt Ebb", 24 "version": (1, 1, 1), 25 "blender": (2, 80, 0), 26 "location": "Image-Window > UVs > Export UV Layout", 27 "description": "Export the UV layout as a 2D graphic", 28 "warning": "", 29 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/mesh_uv_layout.html", 30 "support": 'OFFICIAL', 31 "category": "Import-Export", 32} 33 34 35# @todo write the wiki page 36 37if "bpy" in locals(): 38 import importlib 39 if "export_uv_eps" in locals(): 40 importlib.reload(export_uv_eps) 41 if "export_uv_png" in locals(): 42 importlib.reload(export_uv_png) 43 if "export_uv_svg" in locals(): 44 importlib.reload(export_uv_svg) 45 46import os 47import bpy 48 49from bpy.props import ( 50 StringProperty, 51 BoolProperty, 52 EnumProperty, 53 IntVectorProperty, 54 FloatProperty, 55) 56 57 58class ExportUVLayout(bpy.types.Operator): 59 """Export UV layout to file""" 60 61 bl_idname = "uv.export_layout" 62 bl_label = "Export UV Layout" 63 bl_options = {'REGISTER', 'UNDO'} 64 65 filepath: StringProperty( 66 subtype='FILE_PATH', 67 ) 68 export_all: BoolProperty( 69 name="All UVs", 70 description="Export all UVs in this mesh (not just visible ones)", 71 default=False, 72 ) 73 modified: BoolProperty( 74 name="Modified", 75 description="Exports UVs from the modified mesh", 76 default=False, 77 ) 78 mode: EnumProperty( 79 items=( 80 ('SVG', "Scalable Vector Graphic (.svg)", 81 "Export the UV layout to a vector SVG file"), 82 ('EPS', "Encapsulate PostScript (.eps)", 83 "Export the UV layout to a vector EPS file"), 84 ('PNG', "PNG Image (.png)", 85 "Export the UV layout to a bitmap image"), 86 ), 87 name="Format", 88 description="File format to export the UV layout to", 89 default='PNG', 90 ) 91 size: IntVectorProperty( 92 size=2, 93 default=(1024, 1024), 94 min=8, max=32768, 95 description="Dimensions of the exported file", 96 ) 97 opacity: FloatProperty( 98 name="Fill Opacity", 99 min=0.0, max=1.0, 100 default=0.25, 101 description="Set amount of opacity for exported UV layout", 102 ) 103 # For the file-selector. 104 check_existing: BoolProperty( 105 default=True, 106 options={'HIDDEN'}, 107 ) 108 109 @classmethod 110 def poll(cls, context): 111 obj = context.active_object 112 return obj is not None and obj.type == 'MESH' and obj.data.uv_layers 113 114 def invoke(self, context, event): 115 self.size = self.get_image_size(context) 116 self.filepath = self.get_default_file_name(context) + "." + self.mode.lower() 117 context.window_manager.fileselect_add(self) 118 return {'RUNNING_MODAL'} 119 120 def get_default_file_name(self, context): 121 AMOUNT = 3 122 objects = list(self.iter_objects_to_export(context)) 123 name = " ".join(sorted([obj.name for obj in objects[:AMOUNT]])) 124 if len(objects) > AMOUNT: 125 name += " and more" 126 return name 127 128 def check(self, context): 129 if any(self.filepath.endswith(ext) for ext in (".png", ".eps", ".svg")): 130 self.filepath = self.filepath[:-4] 131 132 ext = "." + self.mode.lower() 133 self.filepath = bpy.path.ensure_ext(self.filepath, ext) 134 return True 135 136 def execute(self, context): 137 obj = context.active_object 138 is_editmode = (obj.mode == 'EDIT') 139 if is_editmode: 140 bpy.ops.object.mode_set(mode='OBJECT', toggle=False) 141 142 filepath = self.filepath 143 filepath = bpy.path.ensure_ext(filepath, "." + self.mode.lower()) 144 145 meshes = list(self.iter_meshes_to_export(context)) 146 polygon_data = list(self.iter_polygon_data_to_draw(context, meshes)) 147 different_colors = set(color for _, color in polygon_data) 148 if self.modified: 149 depsgraph = context.evaluated_depsgraph_get() 150 for obj in self.iter_objects_to_export(context): 151 obj_eval = obj.evaluated_get(depsgraph) 152 obj_eval.to_mesh_clear() 153 154 export = self.get_exporter() 155 export(filepath, polygon_data, different_colors, self.size[0], self.size[1], self.opacity) 156 157 if is_editmode: 158 bpy.ops.object.mode_set(mode='EDIT', toggle=False) 159 160 return {'FINISHED'} 161 162 def iter_meshes_to_export(self, context): 163 depsgraph = context.evaluated_depsgraph_get() 164 for obj in self.iter_objects_to_export(context): 165 if self.modified: 166 yield obj.evaluated_get(depsgraph).to_mesh() 167 else: 168 yield obj.data 169 170 @staticmethod 171 def iter_objects_to_export(context): 172 for obj in {*context.selected_objects, context.active_object}: 173 if obj.type != 'MESH': 174 continue 175 mesh = obj.data 176 if mesh.uv_layers.active is None: 177 continue 178 yield obj 179 180 @staticmethod 181 def currently_image_image_editor(context): 182 return isinstance(context.space_data, bpy.types.SpaceImageEditor) 183 184 def get_currently_opened_image(self, context): 185 if not self.currently_image_image_editor(context): 186 return None 187 return context.space_data.image 188 189 def get_image_size(self, context): 190 # fallback if not in image context 191 image_width = self.size[0] 192 image_height = self.size[1] 193 194 # get size of "active" image if some exist 195 image = self.get_currently_opened_image(context) 196 if image is not None: 197 width, height = image.size 198 if width and height: 199 image_width = width 200 image_height = height 201 202 return image_width, image_height 203 204 def iter_polygon_data_to_draw(self, context, meshes): 205 for mesh in meshes: 206 uv_layer = mesh.uv_layers.active.data 207 for polygon in mesh.polygons: 208 if self.export_all or polygon.select: 209 start = polygon.loop_start 210 end = start + polygon.loop_total 211 uvs = tuple(tuple(uv.uv) for uv in uv_layer[start:end]) 212 yield (uvs, self.get_polygon_color(mesh, polygon)) 213 214 @staticmethod 215 def get_polygon_color(mesh, polygon, default=(0.8, 0.8, 0.8)): 216 if polygon.material_index < len(mesh.materials): 217 material = mesh.materials[polygon.material_index] 218 if material is not None: 219 return tuple(material.diffuse_color)[:3] 220 return default 221 222 def get_exporter(self): 223 if self.mode == 'PNG': 224 from . import export_uv_png 225 return export_uv_png.export 226 elif self.mode == 'EPS': 227 from . import export_uv_eps 228 return export_uv_eps.export 229 elif self.mode == 'SVG': 230 from . import export_uv_svg 231 return export_uv_svg.export 232 else: 233 assert False 234 235 236def menu_func(self, context): 237 self.layout.operator(ExportUVLayout.bl_idname) 238 239 240def register(): 241 bpy.utils.register_class(ExportUVLayout) 242 bpy.types.IMAGE_MT_uvs.append(menu_func) 243 244 245def unregister(): 246 bpy.utils.unregister_class(ExportUVLayout) 247 bpy.types.IMAGE_MT_uvs.remove(menu_func) 248 249 250if __name__ == "__main__": 251 register() 252