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"
8 __date__
= "22 Apr 2022"
13 from bpy
.props
import (
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
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
33 if not common
.is_valid_space(context
, ['IMAGE_EDITOR', 'VIEW_3D']):
36 objs
= common
.get_uv_editable_objects(context
)
40 # only edit mode is allowed to execute
41 if context
.object.mode
!= 'EDIT':
47 def _sort_island_faces(kd
, uvs
, isl1
, isl2
):
53 for f
in isl1
['sorted']:
55 Vector((f
['ave_uv'].x
, f
['ave_uv'].y
, 0.0)))
56 sorted_faces
.append(isl2
['faces'][uvs
[idx
]['face_idx']])
60 def _group_island(island_info
, allowable_center_deviation
,
61 allowable_size_deviation
):
68 # search islands which is not parsed yet
70 for isl_1
in island_info
:
71 if isl_1
['group'] == -1:
74 break # all faces are parsed
77 isl_1
['group'] = num_group
78 isl_1
['sorted'] = isl_1
['faces']
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
88 fabs(dcx
) < allowable_center_deviation
[0]
91 fabs(dcy
) < allowable_center_deviation
[1]
94 fabs(dsx
) < allowable_size_deviation
[0]
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']))
109 (f
['ave_uv'].x
, f
['ave_uv'].y
, 0.0)
112 } for fidx
, f
in enumerate(isl_2
['faces'])
114 for i
, uv
in enumerate(uvs
):
115 kd
.insert(uv
['uv'], i
)
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
124 @PropertyClassRegistry()
129 def init_props(cls
, scene
):
130 scene
.muv_pack_uv_enabled
= BoolProperty(
131 name
="Pack UV Enabled",
132 description
="Pack UV is enabled",
135 scene
.muv_pack_uv_allowable_center_deviation
= FloatVectorProperty(
136 name
="Allowable Center Deviation",
137 description
="Allowable center deviation to judge same UV island",
140 default
=(0.001, 0.001),
144 scene
.muv_pack_uv_allowable_size_deviation
= FloatVectorProperty(
145 name
="Allowable Size Deviation",
146 description
="Allowable sizes deviation to judge same UV island",
149 default
=(0.001, 0.001),
153 scene
.muv_pack_uv_accurate_island_copy
= BoolProperty(
154 name
="Accurate Island Copy",
155 description
="Copy islands topologically",
158 scene
.muv_pack_uv_stride
= FloatVectorProperty(
160 description
="Stride UV coordinates",
167 scene
.muv_pack_uv_apply_pack_uv
= BoolProperty(
168 name
="Apply Pack UV",
169 description
="Apply Pack UV operation intrinsic to Blender itself",
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
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
194 bl_idname
= "uv.muv_pack_uv"
196 bl_description
= "Pack UV (Same UV Islands are integrated)"
197 bl_options
= {'REGISTER', 'UNDO'}
199 rotate
= BoolProperty(
201 description
="Rotate option used by default pack UV function",
203 margin
= FloatProperty(
205 description
="Margin used by default pack UV function",
210 allowable_center_deviation
= FloatVectorProperty(
211 name
="Allowable Center Deviation",
212 description
="Allowable center deviation to judge same UV island",
215 default
=(0.001, 0.001),
219 allowable_size_deviation
= FloatVectorProperty(
220 name
="Allowable Size Deviation",
221 description
="Allowable sizse deviation to judge same UV island",
224 default
=(0.001, 0.001),
228 accurate_island_copy
= BoolProperty(
229 name
="Accurate Island Copy",
230 description
="Copy islands topologically",
233 stride
= FloatVectorProperty(
235 description
="Stride UV coordinates",
242 apply_pack_uv
= BoolProperty(
243 name
="Apply Pack UV",
244 description
="Apply Pack UV operation intrinsic to Blender itself",
249 def poll(cls
, context
):
250 # we can not get area/space/region from console
251 if common
.is_console_mode():
253 return _is_valid_context(context
)
255 def execute(self
, context
):
256 objs
= common
.get_uv_editable_objects(context
)
261 island_to_uv_layer
= {}
262 bm_to_loop_lists
= {}
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"
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
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')
290 for gidx
in range(num_group
):
292 lambda i
, idx
=gidx
: i
['group'] == idx
, island_info
))
293 for f
in group
[0]['faces']:
294 f
['face'].select
= True
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
):
304 lambda i
, idx
=gidx
: i
['group'] == idx
, island_info
))
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
]
312 for f
in group
[0]["faces"]:
313 for l
in f
["face"].loops
:
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
]
325 for l
in f
["face"].loops
:
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
)
340 "Island does not match. "
341 "Disable 'Accurate Island Copy' and try again")
345 for n1
, n2
in pairs
.items():
346 uv1
= n1
.value
["uv_vert"][src_uv_layer
].uv
347 l2
= n2
.value
["loops"]
349 l
[dst_uv_layer
].uv
= uv1
+ uv_stride
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
+ \
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
:
367 bpy
.ops
.uv
.select_all(action
='SELECT')
370 bmesh
.update_edit_mesh(obj
.data
)