1 # SPDX-FileCopyrightText: 2011-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "author": "Campbell Barton, Matt Ebb",
10 "location": "UV Editor > UV > Export UV Layout",
11 "description": "Export the UV layout as a 2D graphic",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/mesh_uv_layout.html",
14 "support": 'OFFICIAL',
15 "category": "Import-Export",
19 # @todo write the wiki page
23 if "export_uv_eps" in locals():
24 importlib
.reload(export_uv_eps
)
25 if "export_uv_png" in locals():
26 importlib
.reload(export_uv_png
)
27 if "export_uv_svg" in locals():
28 importlib
.reload(export_uv_svg
)
33 from bpy
.app
.translations
import contexts
as i18n_contexts
35 from bpy
.props
import (
44 class ExportUVLayout(bpy
.types
.Operator
):
45 """Export UV layout to file"""
47 bl_idname
= "uv.export_layout"
48 bl_label
= "Export UV Layout"
49 bl_options
= {'REGISTER', 'UNDO'}
51 filepath
: StringProperty(
54 export_all
: BoolProperty(
56 description
="Export all UVs in this mesh (not just visible ones)",
59 export_tiles
: EnumProperty(
63 "Export only UVs in the [0, 1] range"),
65 "Export tiles in the UDIM numbering scheme: 1001 + u_tile + 10*v_tile"),
67 "Export tiles in the UVTILE numbering scheme: u(u_tile + 1)_v(v_tile + 1)"),
69 description
="Choose whether to export only the [0, 1] range, or all UV tiles",
72 modified
: BoolProperty(
74 description
="Exports UVs from the modified mesh",
76 translation_context
=i18n_contexts
.id_mesh
,
80 ('SVG', "Scalable Vector Graphic (.svg)",
81 "Export the UV layout to a vector SVG file"),
82 ('EPS', "Encapsulated 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"),
88 description
="File format to export the UV layout to",
91 size
: IntVectorProperty(
96 description
="Dimensions of the exported file",
98 opacity
: FloatProperty(
102 description
="Set amount of opacity for exported UV layout",
104 # For the file-selector.
105 check_existing
: BoolProperty(
111 def poll(cls
, context
):
112 obj
= context
.active_object
113 return obj
is not None and obj
.type == 'MESH' and obj
.data
.uv_layers
115 def invoke(self
, context
, event
):
116 self
.size
= self
.get_image_size(context
)
117 self
.filepath
= self
.get_default_file_name(context
) + "." + self
.mode
.lower()
118 context
.window_manager
.fileselect_add(self
)
119 return {'RUNNING_MODAL'}
121 def get_default_file_name(self
, context
):
123 objects
= list(self
.iter_objects_to_export(context
))
124 name
= " ".join(sorted([obj
.name
for obj
in objects
[:AMOUNT
]]))
125 if len(objects
) > AMOUNT
:
129 def check(self
, context
):
130 if any(self
.filepath
.endswith(ext
) for ext
in (".png", ".eps", ".svg")):
131 self
.filepath
= self
.filepath
[:-4]
133 ext
= "." + self
.mode
.lower()
134 self
.filepath
= bpy
.path
.ensure_ext(self
.filepath
, ext
)
137 def execute(self
, context
):
138 obj
= context
.active_object
139 is_editmode
= (obj
.mode
== 'EDIT')
141 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
143 meshes
= list(self
.iter_meshes_to_export(context
))
144 polygon_data
= list(self
.iter_polygon_data_to_draw(context
, meshes
))
145 different_colors
= set(color
for _
, color
in polygon_data
)
147 depsgraph
= context
.evaluated_depsgraph_get()
148 for obj
in self
.iter_objects_to_export(context
):
149 obj_eval
= obj
.evaluated_get(depsgraph
)
150 obj_eval
.to_mesh_clear()
152 tiles
= self
.tiles_to_export(polygon_data
)
153 export
= self
.get_exporter()
154 dirname
, filename
= os
.path
.split(self
.filepath
)
156 # Strip UDIM or UV numbering, and extension
158 name_regex
= r
"^(.*?)"
159 udim_regex
= r
"(?:\.[0-9]{4})?"
160 uv_regex
= r
"(?:\.u[0-9]+_v[0-9]+)?"
161 ext_regex
= r
"(?:\.png|\.eps|\.svg)?$"
162 if self
.export_tiles
== 'NONE':
163 match
= re
.match(name_regex
+ ext_regex
, filename
)
164 elif self
.export_tiles
== 'UDIM':
165 match
= re
.match(name_regex
+ udim_regex
+ ext_regex
, filename
)
166 elif self
.export_tiles
== 'UV':
167 match
= re
.match(name_regex
+ uv_regex
+ ext_regex
, filename
)
169 filename
= match
.groups()[0]
171 for tile
in sorted(tiles
):
172 filepath
= os
.path
.join(dirname
, filename
)
173 if self
.export_tiles
== 'UDIM':
174 filepath
+= f
".{1001 + tile[0] + tile[1] * 10:04}"
175 elif self
.export_tiles
== 'UV':
176 filepath
+= f
".u{tile[0] + 1}_v{tile[1] + 1}"
177 filepath
= bpy
.path
.ensure_ext(filepath
, "." + self
.mode
.lower())
179 export(filepath
, tile
, polygon_data
, different_colors
,
180 self
.size
[0], self
.size
[1], self
.opacity
)
183 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
187 def iter_meshes_to_export(self
, context
):
188 depsgraph
= context
.evaluated_depsgraph_get()
189 for obj
in self
.iter_objects_to_export(context
):
191 yield obj
.evaluated_get(depsgraph
).to_mesh()
196 def iter_objects_to_export(context
):
197 for obj
in {*context
.selected_objects
, context
.active_object
}:
198 if obj
.type != 'MESH':
201 if mesh
.uv_layers
.active
is None:
205 def tiles_to_export(self
, polygon_data
):
206 """Get a set of tiles containing UVs.
207 This assumes there is no UV edge crossing an otherwise empty tile.
209 if self
.export_tiles
== 'NONE':
212 from math
import floor
214 for poly
in polygon_data
:
216 # Ignore UVs at corners - precisely touching the right or upper edge
217 # of a tile should not load its right/upper neighbor as well.
218 # From intern/cycles/scene/attribute.cpp
220 x
, y
= floor(u
), floor(v
)
221 if x
> 0 and u
< x
+ 1e-6:
223 if y
> 0 and v
< y
+ 1e-6:
225 if x
>= 0 and y
>= 0:
230 def currently_image_image_editor(context
):
231 return isinstance(context
.space_data
, bpy
.types
.SpaceImageEditor
)
233 def get_currently_opened_image(self
, context
):
234 if not self
.currently_image_image_editor(context
):
236 return context
.space_data
.image
238 def get_image_size(self
, context
):
239 # fallback if not in image context
240 image_width
= self
.size
[0]
241 image_height
= self
.size
[1]
243 # get size of "active" image if some exist
244 image
= self
.get_currently_opened_image(context
)
245 if image
is not None:
246 width
, height
= image
.size
249 image_height
= height
251 return image_width
, image_height
253 def iter_polygon_data_to_draw(self
, context
, meshes
):
255 uv_layer
= mesh
.uv_layers
.active
.data
256 for polygon
in mesh
.polygons
:
257 if self
.export_all
or polygon
.select
:
258 start
= polygon
.loop_start
259 end
= start
+ polygon
.loop_total
260 uvs
= tuple(tuple(uv
.uv
) for uv
in uv_layer
[start
:end
])
261 yield (uvs
, self
.get_polygon_color(mesh
, polygon
))
264 def get_polygon_color(mesh
, polygon
, default
=(0.8, 0.8, 0.8)):
265 if polygon
.material_index
< len(mesh
.materials
):
266 material
= mesh
.materials
[polygon
.material_index
]
267 if material
is not None:
268 return tuple(material
.diffuse_color
)[:3]
271 def get_exporter(self
):
272 if self
.mode
== 'PNG':
273 from . import export_uv_png
274 return export_uv_png
.export
275 elif self
.mode
== 'EPS':
276 from . import export_uv_eps
277 return export_uv_eps
.export
278 elif self
.mode
== 'SVG':
279 from . import export_uv_svg
280 return export_uv_svg
.export
285 def menu_func(self
, context
):
286 self
.layout
.operator(ExportUVLayout
.bl_idname
)
290 bpy
.utils
.register_class(ExportUVLayout
)
291 bpy
.types
.IMAGE_MT_uvs
.append(menu_func
)
295 bpy
.utils
.unregister_class(ExportUVLayout
)
296 bpy
.types
.IMAGE_MT_uvs
.remove(menu_func
)
299 if __name__
== "__main__":