Fix: Node Wrangler: new reroutes offset when output hidden
[blender-addons.git] / magic_uv / op / pack_uv.py
blob2b24970d6eaf9979e5e7fc056a1e7937e92116bd
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 __author__ = "Nutti <nutti.metro@gmail.com>"
4 __status__ = "production"
5 __version__ = "6.6"
6 __date__ = "22 Apr 2022"
8 from math import fabs
10 import bpy
11 from bpy.props import (
12 FloatProperty,
13 FloatVectorProperty,
14 BoolProperty,
16 import bmesh
17 import mathutils
18 from mathutils import Vector
20 from ..utils.bl_class_registry import BlClassRegistry
21 from ..utils.property_class_registry import PropertyClassRegistry
22 from ..utils.graph import graph_is_isomorphic
23 from ..utils import compatibility as compat
24 from .. import common
27 def _is_valid_context(context):
28 # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
29 # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
30 # after the execution
31 if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']):
32 return False
34 objs = common.get_uv_editable_objects(context)
35 if not objs:
36 return False
38 # only edit mode is allowed to execute
39 if context.object.mode != 'EDIT':
40 return False
42 return True
45 def _sort_island_faces(kd, uvs, isl1, isl2):
46 """
47 Sort faces in island
48 """
50 sorted_faces = []
51 for f in isl1['sorted']:
52 _, idx, _ = kd.find(
53 Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0)))
54 sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']])
55 return sorted_faces
58 def _group_island(island_info, allowable_center_deviation,
59 allowable_size_deviation):
60 """
61 Group island
62 """
64 num_group = 0
65 while True:
66 # search islands which is not parsed yet
67 isl_1 = None
68 for isl_1 in island_info:
69 if isl_1['group'] == -1:
70 break
71 else:
72 break # all faces are parsed
73 if isl_1 is None:
74 break
75 isl_1['group'] = num_group
76 isl_1['sorted'] = isl_1['faces']
78 # search same island
79 for isl_2 in island_info:
80 if isl_2['group'] == -1:
81 dcx = isl_2['center'].x - isl_1['center'].x
82 dcy = isl_2['center'].y - isl_1['center'].y
83 dsx = isl_2['size'].x - isl_1['size'].x
84 dsy = isl_2['size'].y - isl_1['size'].y
85 center_x_matched = (
86 fabs(dcx) < allowable_center_deviation[0]
88 center_y_matched = (
89 fabs(dcy) < allowable_center_deviation[1]
91 size_x_matched = (
92 fabs(dsx) < allowable_size_deviation[0]
94 size_y_matched = (
95 fabs(dsy) < allowable_size_deviation[1]
97 center_matched = center_x_matched and center_y_matched
98 size_matched = size_x_matched and size_y_matched
99 num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv'])
100 # are islands have same?
101 if center_matched and size_matched and num_uv_matched:
102 isl_2['group'] = num_group
103 kd = mathutils.kdtree.KDTree(len(isl_2['faces']))
104 uvs = [
106 'uv': Vector(
107 (f['ave_uv'].x, f['ave_uv'].y, 0.0)
109 'face_idx': fidx
110 } for fidx, f in enumerate(isl_2['faces'])
112 for i, uv in enumerate(uvs):
113 kd.insert(uv['uv'], i)
114 kd.balance()
115 # sort faces for copy/paste UV
116 isl_2['sorted'] = _sort_island_faces(kd, uvs, isl_1, isl_2)
117 num_group = num_group + 1
119 return num_group
122 @PropertyClassRegistry()
123 class _Properties:
124 idname = "pack_uv"
126 @classmethod
127 def init_props(cls, scene):
128 scene.muv_pack_uv_enabled = BoolProperty(
129 name="Pack UV Enabled",
130 description="Pack UV is enabled",
131 default=False
133 scene.muv_pack_uv_allowable_center_deviation = FloatVectorProperty(
134 name="Allowable Center Deviation",
135 description="Allowable center deviation to judge same UV island",
136 min=0.000001,
137 max=10.0,
138 default=(0.001, 0.001),
139 size=2,
140 subtype='XYZ'
142 scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty(
143 name="Allowable Size Deviation",
144 description="Allowable sizes deviation to judge same UV island",
145 min=0.000001,
146 max=10.0,
147 default=(0.001, 0.001),
148 size=2,
149 subtype='XYZ'
151 scene.muv_pack_uv_accurate_island_copy = BoolProperty(
152 name="Accurate Island Copy",
153 description="Copy islands topologically",
154 default=True
156 scene.muv_pack_uv_stride = FloatVectorProperty(
157 name="Stride",
158 description="Stride UV coordinates",
159 min=-100.0,
160 max=100.0,
161 default=(0.0, 0.0),
162 size=2,
163 subtype='XYZ'
165 scene.muv_pack_uv_apply_pack_uv = BoolProperty(
166 name="Apply Pack UV",
167 description="Apply Pack UV operation intrinsic to Blender itself",
168 default=True
171 @classmethod
172 def del_props(cls, scene):
173 del scene.muv_pack_uv_enabled
174 del scene.muv_pack_uv_allowable_center_deviation
175 del scene.muv_pack_uv_allowable_size_deviation
176 del scene.muv_pack_uv_accurate_island_copy
177 del scene.muv_pack_uv_stride
178 del scene.muv_pack_uv_apply_pack_uv
181 @BlClassRegistry()
182 @compat.make_annotations
183 class MUV_OT_PackUV(bpy.types.Operator):
185 Operation class: Pack UV with same UV islands are integrated
186 Island matching algorithm
187 - Same center of UV island
188 - Same size of UV island
189 - Same number of UV
192 bl_idname = "uv.muv_pack_uv"
193 bl_label = "Pack UV"
194 bl_description = "Pack UV (Same UV Islands are integrated)"
195 bl_options = {'REGISTER', 'UNDO'}
197 rotate = BoolProperty(
198 name="Rotate",
199 description="Rotate option used by default pack UV function",
200 default=False)
201 margin = FloatProperty(
202 name="Margin",
203 description="Margin used by default pack UV function",
204 min=0,
205 max=1,
206 default=0.001
208 allowable_center_deviation = FloatVectorProperty(
209 name="Allowable Center Deviation",
210 description="Allowable center deviation to judge same UV island",
211 min=0.000001,
212 max=10.0,
213 default=(0.001, 0.001),
214 size=2,
215 subtype='XYZ'
217 allowable_size_deviation = FloatVectorProperty(
218 name="Allowable Size Deviation",
219 description="Allowable sizse deviation to judge same UV island",
220 min=0.000001,
221 max=10.0,
222 default=(0.001, 0.001),
223 size=2,
224 subtype='XYZ'
226 accurate_island_copy = BoolProperty(
227 name="Accurate Island Copy",
228 description="Copy islands topologically",
229 default=True
231 stride = FloatVectorProperty(
232 name="Stride",
233 description="Stride UV coordinates",
234 min=-100.0,
235 max=100.0,
236 default=(0.0, 0.0),
237 size=2,
238 subtype='XYZ'
240 apply_pack_uv = BoolProperty(
241 name="Apply Pack UV",
242 description="Apply Pack UV operation intrinsic to Blender itself",
243 default=True
246 @classmethod
247 def poll(cls, context):
248 # we can not get area/space/region from console
249 if common.is_console_mode():
250 return True
251 return _is_valid_context(context)
253 def execute(self, context):
254 objs = common.get_uv_editable_objects(context)
256 island_info = []
257 selected_faces = []
258 island_to_bm = {}
259 island_to_uv_layer = {}
260 bm_to_loop_lists = {}
261 for obj in objs:
262 bm = bmesh.from_edit_mesh(obj.data)
263 if common.check_version(2, 73, 0) >= 0:
264 bm.faces.ensure_lookup_table()
265 if not bm.loops.layers.uv:
266 self.report({'WARNING'},
267 "Object {} must have more than one UV map"
268 .format(obj.name))
269 return {'CANCELLED'}
270 uv_layer = bm.loops.layers.uv.verify()
272 selected_faces.extend([f for f in bm.faces if f.select])
273 isl = common.get_island_info(obj)
274 for i, info in enumerate(isl):
275 id_ = i + len(island_info)
276 island_to_bm[id_] = bm
277 island_to_uv_layer[id_] = uv_layer
278 info["id"] = id_
279 island_info.extend(isl)
280 bm_to_loop_lists[bm] = [l for f in bm.faces for l in f.loops]
282 num_group = _group_island(island_info,
283 self.allowable_center_deviation,
284 self.allowable_size_deviation)
285 bpy.ops.mesh.select_all(action='DESELECT')
287 # pack UV
288 for gidx in range(num_group):
289 group = list(filter(
290 lambda i, idx=gidx: i['group'] == idx, island_info))
291 for f in group[0]['faces']:
292 f['face'].select = True
293 for obj in objs:
294 bmesh.update_edit_mesh(obj.data)
295 bpy.ops.uv.select_all(action='SELECT')
296 if self.apply_pack_uv:
297 bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin)
299 # copy/paste UV among same islands
300 for gidx in range(num_group):
301 group = list(filter(
302 lambda i, idx=gidx: i['group'] == idx, island_info))
303 if len(group) <= 1:
304 continue
305 src_bm = island_to_bm[group[0]["id"]]
306 src_uv_layer = island_to_uv_layer[group[0]["id"]]
307 src_loop_lists = bm_to_loop_lists[src_bm]
309 src_loops = []
310 for f in group[0]["faces"]:
311 for l in f["face"].loops:
312 src_loops.append(l)
314 src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer)
316 for stride_idx, g in enumerate(group[1:]):
317 dst_bm = island_to_bm[g["id"]]
318 dst_uv_layer = island_to_uv_layer[g["id"]]
319 dst_loop_lists = bm_to_loop_lists[dst_bm]
321 dst_loops = []
322 for f in g["faces"]:
323 for l in f["face"].loops:
324 dst_loops.append(l)
326 dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer)
328 uv_stride = Vector(((stride_idx + 1) * self.stride.x,
329 (stride_idx + 1) * self.stride.y))
330 if self.accurate_island_copy:
331 # Check if the graph is isomorphic.
332 # If the graph is isomorphic, matching pair is returned.
333 result, pairs = graph_is_isomorphic(
334 src_uv_graph, dst_uv_graph)
335 if not result:
336 self.report(
337 {'WARNING'},
338 "Island does not match. "
339 "Disable 'Accurate Island Copy' and try again")
340 return {'CANCELLED'}
342 # Paste UV island.
343 for n1, n2 in pairs.items():
344 uv1 = n1.value["uv_vert"][src_uv_layer].uv
345 l2 = n2.value["loops"]
346 for l in l2:
347 l[dst_uv_layer].uv = uv1 + uv_stride
348 else:
349 for (src_face, dest_face) in zip(
350 group[0]['sorted'], g['sorted']):
351 for (src_loop, dest_loop) in zip(
352 src_face['face'].loops,
353 dest_face['face'].loops):
354 src_lidx = src_loop.index
355 dst_lidx = dest_loop.index
356 dst_loop_lists[dst_lidx][dst_uv_layer].uv = \
357 src_loop_lists[src_lidx][src_uv_layer].uv + \
358 uv_stride
360 # restore face/UV selection
361 bpy.ops.uv.select_all(action='DESELECT')
362 bpy.ops.mesh.select_all(action='DESELECT')
363 for f in selected_faces:
364 f.select = True
365 bpy.ops.uv.select_all(action='SELECT')
367 for obj in objs:
368 bmesh.update_edit_mesh(obj.data)
370 return {'FINISHED'}