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