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