File headers: use SPDX license identifiers
[blender-addons.git] / magic_uv / op / preserve_uv_aspect.py
blob9d3cbddeb0608387a749b45a077b226c2d44d336
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # <pep8-80 compliant>
5 __author__ = "Nutti <nutti.metro@gmail.com>"
6 __status__ = "production"
7 __version__ = "6.5"
8 __date__ = "6 Mar 2021"
10 import bpy
11 from bpy.props import StringProperty, EnumProperty, BoolProperty
12 import bmesh
13 from mathutils import Vector
15 from .. import common
16 from ..utils.bl_class_registry import BlClassRegistry
17 from ..utils.property_class_registry import PropertyClassRegistry
18 from ..utils import compatibility as compat
21 def _is_valid_context(context):
22 objs = common.get_uv_editable_objects(context)
23 if not objs:
24 return False
26 # only edit mode is allowed to execute
27 if context.object.mode != 'EDIT':
28 return False
30 # only 'VIEW_3D' space is allowed to execute
31 if not common.is_valid_space(context, ['VIEW_3D']):
32 return False
34 return True
37 @PropertyClassRegistry()
38 class _Properties:
39 idname = "preserve_uv_aspect"
41 @classmethod
42 def init_props(cls, scene):
43 def get_loaded_texture_name(_, __):
44 items = [(key, key, "") for key in bpy.data.images.keys()]
45 items.append(("None", "None", ""))
46 return items
48 scene.muv_preserve_uv_aspect_enabled = BoolProperty(
49 name="Preserve UV Aspect Enabled",
50 description="Preserve UV Aspect is enabled",
51 default=False
53 scene.muv_preserve_uv_aspect_tex_image = EnumProperty(
54 name="Image",
55 description="Texture Image",
56 items=get_loaded_texture_name
58 scene.muv_preserve_uv_aspect_origin = EnumProperty(
59 name="Origin",
60 description="Aspect Origin",
61 items=[
62 ('CENTER', 'Center', 'Center'),
63 ('LEFT_TOP', 'Left Top', 'Left Bottom'),
64 ('LEFT_CENTER', 'Left Center', 'Left Center'),
65 ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
66 ('CENTER_TOP', 'Center Top', 'Center Top'),
67 ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
68 ('RIGHT_TOP', 'Right Top', 'Right Top'),
69 ('RIGHT_CENTER', 'Right Center', 'Right Center'),
70 ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
73 default="CENTER"
76 @classmethod
77 def del_props(cls, scene):
78 del scene.muv_preserve_uv_aspect_enabled
79 del scene.muv_preserve_uv_aspect_tex_image
80 del scene.muv_preserve_uv_aspect_origin
83 @BlClassRegistry()
84 @compat.make_annotations
85 class MUV_OT_PreserveUVAspect(bpy.types.Operator):
86 """
87 Operation class: Preserve UV Aspect
88 """
90 bl_idname = "uv.muv_preserve_uv_aspect"
91 bl_label = "Preserve UV Aspect"
92 bl_description = "Choose Image"
93 bl_options = {'REGISTER', 'UNDO'}
95 dest_img_name = StringProperty(options={'HIDDEN'})
96 origin = EnumProperty(
97 name="Origin",
98 description="Aspect Origin",
99 items=[
100 ('CENTER', 'Center', 'Center'),
101 ('LEFT_TOP', 'Left Top', 'Left Bottom'),
102 ('LEFT_CENTER', 'Left Center', 'Left Center'),
103 ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
104 ('CENTER_TOP', 'Center Top', 'Center Top'),
105 ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
106 ('RIGHT_TOP', 'Right Top', 'Right Top'),
107 ('RIGHT_CENTER', 'Right Center', 'Right Center'),
108 ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
111 default="CENTER"
114 @classmethod
115 def poll(cls, context):
116 # we can not get area/space/region from console
117 if common.is_console_mode():
118 return True
119 return _is_valid_context(context)
121 def execute(self, context):
122 # Note: the current system only works if the
123 # f[tex_layer].image doesn't return None
124 # which will happen in certain cases
125 objs = common.get_uv_editable_objects(context)
127 obj_list = {} # { Material: Object }
128 for obj in objs:
129 if common.check_version(2, 80, 0) >= 0:
130 # If more than two selected objects shares same
131 # material, we need to calculate new UV coordinates
132 # before image on texture node is overwritten.
133 material_to_rewrite = []
134 for slot in obj.material_slots:
135 if not slot.material:
136 continue
137 nodes = common.find_texture_nodes_from_material(
138 slot.material)
139 if len(nodes) >= 2:
140 self.report(
141 {'WARNING'},
142 "Object {} must not have more than 2 "
143 "shader nodes with image texture"
144 .format(obj.name))
145 return {'CANCELLED'}
146 if not nodes:
147 continue
148 material_to_rewrite.append(slot.material)
150 if len(material_to_rewrite) >= 2:
151 self.report(
152 {'WARNING'},
153 "Object {} must not have more than 2 "
154 "materials with image texture"
155 .format(obj.name))
156 return {'CANCELLED'}
157 if len(material_to_rewrite) == 0:
158 self.report(
159 {'WARNING'},
160 "Object {} must not have more than 1 "
161 "material with image texture"
162 .format(obj.name))
163 return {'CANCELLED'}
164 if material_to_rewrite[0] not in obj_list.keys():
165 obj_list[material_to_rewrite[0]] = []
166 obj_list[material_to_rewrite[0]].append(obj)
167 else:
168 # If blender version is < (2, 79), multiple objects editing
169 # mode is not supported. So, we add dummy key to obj_list.
170 obj_list["Dummy"] = [obj]
172 # pylint: disable=R1702
173 for mtrl, o in obj_list.items():
174 for obj in o:
175 bm = bmesh.from_edit_mesh(obj.data)
177 if common.check_version(2, 73, 0) >= 0:
178 bm.faces.ensure_lookup_table()
180 if not bm.loops.layers.uv:
181 self.report({'WARNING'},
182 "Object must have more than one UV map")
183 return {'CANCELLED'}
185 uv_layer = bm.loops.layers.uv.verify()
187 sel_faces = [f for f in bm.faces if f.select]
188 dest_img = bpy.data.images[self.dest_img_name]
190 info = {}
192 if compat.check_version(2, 80, 0) >= 0:
193 tex_image = common.find_image(obj)
194 for f in sel_faces:
195 if tex_image not in info.keys():
196 info[tex_image] = {}
197 info[tex_image]['faces'] = []
198 info[tex_image]['faces'].append(f)
199 else:
200 tex_layer = bm.faces.layers.tex.verify()
201 for f in sel_faces:
202 if not f[tex_layer].image in info.keys():
203 info[f[tex_layer].image] = {}
204 info[f[tex_layer].image]['faces'] = []
205 info[f[tex_layer].image]['faces'].append(f)
207 for img in info:
208 if img is None:
209 continue
211 src_img = img
212 ratio = Vector((
213 dest_img.size[0] / src_img.size[0],
214 dest_img.size[1] / src_img.size[1]))
216 if self.origin == 'CENTER':
217 origin = Vector((0.0, 0.0))
218 num = 0
219 for f in info[img]['faces']:
220 for l in f.loops:
221 uv = l[uv_layer].uv
222 origin = origin + uv
223 num = num + 1
224 origin = origin / num
225 elif self.origin == 'LEFT_TOP':
226 origin = Vector((100000.0, -100000.0))
227 for f in info[img]['faces']:
228 for l in f.loops:
229 uv = l[uv_layer].uv
230 origin.x = min(origin.x, uv.x)
231 origin.y = max(origin.y, uv.y)
232 elif self.origin == 'LEFT_CENTER':
233 origin = Vector((100000.0, 0.0))
234 num = 0
235 for f in info[img]['faces']:
236 for l in f.loops:
237 uv = l[uv_layer].uv
238 origin.x = min(origin.x, uv.x)
239 origin.y = origin.y + uv.y
240 num = num + 1
241 origin.y = origin.y / num
242 elif self.origin == 'LEFT_BOTTOM':
243 origin = Vector((100000.0, 100000.0))
244 for f in info[img]['faces']:
245 for l in f.loops:
246 uv = l[uv_layer].uv
247 origin.x = min(origin.x, uv.x)
248 origin.y = min(origin.y, uv.y)
249 elif self.origin == 'CENTER_TOP':
250 origin = Vector((0.0, -100000.0))
251 num = 0
252 for f in info[img]['faces']:
253 for l in f.loops:
254 uv = l[uv_layer].uv
255 origin.x = origin.x + uv.x
256 origin.y = max(origin.y, uv.y)
257 num = num + 1
258 origin.x = origin.x / num
259 elif self.origin == 'CENTER_BOTTOM':
260 origin = Vector((0.0, 100000.0))
261 num = 0
262 for f in info[img]['faces']:
263 for l in f.loops:
264 uv = l[uv_layer].uv
265 origin.x = origin.x + uv.x
266 origin.y = min(origin.y, uv.y)
267 num = num + 1
268 origin.x = origin.x / num
269 elif self.origin == 'RIGHT_TOP':
270 origin = Vector((-100000.0, -100000.0))
271 for f in info[img]['faces']:
272 for l in f.loops:
273 uv = l[uv_layer].uv
274 origin.x = max(origin.x, uv.x)
275 origin.y = max(origin.y, uv.y)
276 elif self.origin == 'RIGHT_CENTER':
277 origin = Vector((-100000.0, 0.0))
278 num = 0
279 for f in info[img]['faces']:
280 for l in f.loops:
281 uv = l[uv_layer].uv
282 origin.x = max(origin.x, uv.x)
283 origin.y = origin.y + uv.y
284 num = num + 1
285 origin.y = origin.y / num
286 elif self.origin == 'RIGHT_BOTTOM':
287 origin = Vector((-100000.0, 100000.0))
288 for f in info[img]['faces']:
289 for l in f.loops:
290 uv = l[uv_layer].uv
291 origin.x = max(origin.x, uv.x)
292 origin.y = min(origin.y, uv.y)
293 else:
294 self.report({'ERROR'}, "Unknown Operation")
295 return {'CANCELLED'}
297 info[img]['ratio'] = ratio
298 info[img]['origin'] = origin
300 for img in info:
301 if img is None:
302 continue
304 for f in info[img]['faces']:
305 if compat.check_version(2, 80, 0) < 0:
306 tex_layer = bm.faces.layers.tex.verify()
307 f[tex_layer].image = dest_img
308 for l in f.loops:
309 uv = l[uv_layer].uv
310 origin = info[img]['origin']
311 ratio = info[img]['ratio']
312 diff = uv - origin
313 diff.x = diff.x / ratio.x
314 diff.y = diff.y / ratio.y
315 uv.x = origin.x + diff.x
316 uv.y = origin.y + diff.y
317 l[uv_layer].uv = uv
319 bmesh.update_edit_mesh(obj.data)
321 if compat.check_version(2, 80, 0) >= 0:
322 nodes = common.find_texture_nodes_from_material(mtrl)
323 nodes[0].image = dest_img
325 return {'FINISHED'}