Refactor: Node Wrangler: PreviewNode operator
[blender-addons.git] / io_mesh_uv_layout / __init__.py
blobf2b59ace3d1bd3658b58b5e3907ea1fbe469ad6b
1 # SPDX-FileCopyrightText: 2011-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "UV Layout",
7 "author": "Campbell Barton, Matt Ebb",
8 "version": (1, 2, 0),
9 "blender": (3, 0, 0),
10 "location": "UV Editor > UV > Export UV Layout",
11 "description": "Export the UV layout as a 2D graphic",
12 "warning": "",
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
21 if "bpy" in locals():
22 import importlib
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)
30 import os
31 import bpy
33 from bpy.app.translations import contexts as i18n_contexts
35 from bpy.props import (
36 StringProperty,
37 BoolProperty,
38 EnumProperty,
39 IntVectorProperty,
40 FloatProperty,
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(
52 subtype='FILE_PATH',
54 export_all: BoolProperty(
55 name="All UVs",
56 description="Export all UVs in this mesh (not just visible ones)",
57 default=False,
59 export_tiles: EnumProperty(
60 name="Export Tiles",
61 items=(
62 ('NONE', "None",
63 "Export only UVs in the [0, 1] range"),
64 ('UDIM', "UDIM",
65 "Export tiles in the UDIM numbering scheme: 1001 + u_tile + 10*v_tile"),
66 ('UV', "UVTILE",
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",
70 default='NONE',
72 modified: BoolProperty(
73 name="Modified",
74 description="Exports UVs from the modified mesh",
75 default=False,
76 translation_context=i18n_contexts.id_mesh,
78 mode: EnumProperty(
79 items=(
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"),
87 name="Format",
88 description="File format to export the UV layout to",
89 default='PNG',
91 size: IntVectorProperty(
92 name="Size",
93 size=2,
94 default=(1024, 1024),
95 min=8, max=32768,
96 description="Dimensions of the exported file",
98 opacity: FloatProperty(
99 name="Fill Opacity",
100 min=0.0, max=1.0,
101 default=0.25,
102 description="Set amount of opacity for exported UV layout",
104 # For the file-selector.
105 check_existing: BoolProperty(
106 default=True,
107 options={'HIDDEN'},
110 @classmethod
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):
122 AMOUNT = 3
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:
126 name += " and more"
127 return name
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)
135 return True
137 def execute(self, context):
138 obj = context.active_object
139 is_editmode = (obj.mode == 'EDIT')
140 if is_editmode:
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)
146 if self.modified:
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
157 import re
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)
168 if match:
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)
182 if is_editmode:
183 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
185 return {'FINISHED'}
187 def iter_meshes_to_export(self, context):
188 depsgraph = context.evaluated_depsgraph_get()
189 for obj in self.iter_objects_to_export(context):
190 if self.modified:
191 yield obj.evaluated_get(depsgraph).to_mesh()
192 else:
193 yield obj.data
195 @staticmethod
196 def iter_objects_to_export(context):
197 for obj in {*context.selected_objects, context.active_object}:
198 if obj.type != 'MESH':
199 continue
200 mesh = obj.data
201 if mesh.uv_layers.active is None:
202 continue
203 yield obj
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':
210 return {(0, 0)}
212 from math import floor
213 tiles = set()
214 for poly in polygon_data:
215 for uv in poly[0]:
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
219 u, v = uv[0], uv[1]
220 x, y = floor(u), floor(v)
221 if x > 0 and u < x + 1e-6:
222 x -= 1
223 if y > 0 and v < y + 1e-6:
224 y -= 1
225 if x >= 0 and y >= 0:
226 tiles.add((x, y))
227 return tiles
229 @staticmethod
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):
235 return None
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
247 if width and height:
248 image_width = width
249 image_height = height
251 return image_width, image_height
253 def iter_polygon_data_to_draw(self, context, meshes):
254 for mesh in 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))
263 @staticmethod
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]
269 return default
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
281 else:
282 assert False
285 def menu_func(self, context):
286 self.layout.operator(ExportUVLayout.bl_idname)
289 def register():
290 bpy.utils.register_class(ExportUVLayout)
291 bpy.types.IMAGE_MT_uvs.append(menu_func)
294 def unregister():
295 bpy.utils.unregister_class(ExportUVLayout)
296 bpy.types.IMAGE_MT_uvs.remove(menu_func)
299 if __name__ == "__main__":
300 register()