Update for 2.8
[blender-addons.git] / uv_magic_uv / op / texture_lock.py
blobd6c56f5afc4be2e584f8c9105f1dca5e43c5c795
1 # <pep8-80 compliant>
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 __author__ = "Nutti <nutti.metro@gmail.com>"
22 __status__ = "production"
23 __version__ = "5.1"
24 __date__ = "24 Feb 2018"
26 import math
27 from math import atan2, cos, sqrt, sin, fabs
29 import bpy
30 import bmesh
31 from mathutils import Vector
32 from bpy.props import BoolProperty
34 from .. import common
37 def get_vco(verts_orig, loop):
38 """
39 Get vertex original coordinate from loop
40 """
41 for vo in verts_orig:
42 if vo["vidx"] == loop.vert.index and vo["moved"] is False:
43 return vo["vco"]
44 return loop.vert.co
47 def get_link_loops(vert):
48 """
49 Get loop linked to vertex
50 """
51 link_loops = []
52 for f in vert.link_faces:
53 adj_loops = []
54 for loop in f.loops:
55 # self loop
56 if loop.vert == vert:
57 l = loop
58 # linked loop
59 else:
60 for e in loop.vert.link_edges:
61 if e.other_vert(loop.vert) == vert:
62 adj_loops.append(loop)
63 if len(adj_loops) < 2:
64 return None
66 link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]})
67 return link_loops
70 def get_ini_geom(link_loop, uv_layer, verts_orig, v_orig):
71 """
72 Get initial geometory
73 (Get interior angle of face in vertex/UV space)
74 """
75 u = link_loop["l"][uv_layer].uv
76 v0 = get_vco(verts_orig, link_loop["l0"])
77 u0 = link_loop["l0"][uv_layer].uv
78 v1 = get_vco(verts_orig, link_loop["l1"])
79 u1 = link_loop["l1"][uv_layer].uv
81 # get interior angle of face in vertex space
82 v0v1 = v1 - v0
83 v0v = v_orig["vco"] - v0
84 v1v = v_orig["vco"] - v1
85 theta0 = v0v1.angle(v0v)
86 theta1 = v0v1.angle(-v1v)
87 if (theta0 + theta1) > math.pi:
88 theta0 = v0v1.angle(-v0v)
89 theta1 = v0v1.angle(v1v)
91 # get interior angle of face in UV space
92 u0u1 = u1 - u0
93 u0u = u - u0
94 u1u = u - u1
95 phi0 = u0u1.angle(u0u)
96 phi1 = u0u1.angle(-u1u)
97 if (phi0 + phi1) > math.pi:
98 phi0 = u0u1.angle(-u0u)
99 phi1 = u0u1.angle(u1u)
101 # get direction of linked UV coordinate
102 # this will be used to judge whether angle is more or less than 180 degree
103 dir0 = u0u1.cross(u0u) > 0
104 dir1 = u0u1.cross(u1u) > 0
106 return {
107 "theta0": theta0,
108 "theta1": theta1,
109 "phi0": phi0,
110 "phi1": phi1,
111 "dir0": dir0,
112 "dir1": dir1}
115 def get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom):
117 Get target UV coordinate
119 v0 = get_vco(verts_orig, link_loop["l0"])
120 lo0 = link_loop["l0"]
121 v1 = get_vco(verts_orig, link_loop["l1"])
122 lo1 = link_loop["l1"]
124 # get interior angle of face in vertex space
125 v0v1 = v1 - v0
126 v0v = v.co - v0
127 v1v = v.co - v1
128 theta0 = v0v1.angle(v0v)
129 theta1 = v0v1.angle(-v1v)
130 if (theta0 + theta1) > math.pi:
131 theta0 = v0v1.angle(-v0v)
132 theta1 = v0v1.angle(v1v)
134 # calculate target interior angle in UV space
135 phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"]
136 phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"]
138 uv0 = lo0[uv_layer].uv
139 uv1 = lo1[uv_layer].uv
141 # calculate target vertex coordinate from target interior angle
142 tuv0, tuv1 = calc_tri_vert(uv0, uv1, phi0, phi1)
144 # target UV coordinate depends on direction, so judge using direction of
145 # linked UV coordinate
146 u0u1 = uv1 - uv0
147 u0u = tuv0 - uv0
148 u1u = tuv0 - uv1
149 dir0 = u0u1.cross(u0u) > 0
150 dir1 = u0u1.cross(u1u) > 0
151 if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1):
152 return tuv1
154 return tuv0
157 def calc_tri_vert(v0, v1, angle0, angle1):
159 Calculate rest coordinate from other coordinates and angle of end
161 angle = math.pi - angle0 - angle1
163 alpha = atan2(v1.y - v0.y, v1.x - v0.x)
164 d = (v1.x - v0.x) / cos(alpha)
165 a = d * sin(angle0) / sin(angle)
166 b = d * sin(angle1) / sin(angle)
167 s = (a + b + d) / 2.0
168 if fabs(d) < 0.0000001:
169 xd = 0
170 yd = 0
171 else:
172 xd = (b * b - a * a + d * d) / (2 * d)
173 yd = 2 * sqrt(s * (s - a) * (s - b) * (s - d)) / d
174 x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x
175 y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y
176 x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x
177 y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y
179 return Vector((x1, y1)), Vector((x2, y2))
182 class MUV_TexLockStart(bpy.types.Operator):
184 Operation class: Start Texture Lock
187 bl_idname = "uv.muv_texlock_start"
188 bl_label = "Start"
189 bl_description = "Start Texture Lock"
190 bl_options = {'REGISTER', 'UNDO'}
192 def execute(self, context):
193 props = context.scene.muv_props.texlock
194 obj = bpy.context.active_object
195 bm = bmesh.from_edit_mesh(obj.data)
196 if common.check_version(2, 73, 0) >= 0:
197 bm.verts.ensure_lookup_table()
198 bm.edges.ensure_lookup_table()
199 bm.faces.ensure_lookup_table()
201 if not bm.loops.layers.uv:
202 self.report(
203 {'WARNING'}, "Object must have more than one UV map")
204 return {'CANCELLED'}
206 props.verts_orig = [
207 {"vidx": v.index, "vco": v.co.copy(), "moved": False}
208 for v in bm.verts if v.select]
210 return {'FINISHED'}
213 class MUV_TexLockStop(bpy.types.Operator):
215 Operation class: Stop Texture Lock
218 bl_idname = "uv.muv_texlock_stop"
219 bl_label = "Stop"
220 bl_description = "Stop Texture Lock"
221 bl_options = {'REGISTER', 'UNDO'}
223 connect = BoolProperty(
224 name="Connect UV",
225 default=True
228 def execute(self, context):
229 sc = context.scene
230 props = sc.muv_props.texlock
231 obj = bpy.context.active_object
232 bm = bmesh.from_edit_mesh(obj.data)
233 if common.check_version(2, 73, 0) >= 0:
234 bm.verts.ensure_lookup_table()
235 bm.edges.ensure_lookup_table()
236 bm.faces.ensure_lookup_table()
238 if not bm.loops.layers.uv:
239 self.report(
240 {'WARNING'}, "Object must have more than one UV map")
241 return {'CANCELLED'}
242 uv_layer = bm.loops.layers.uv.verify()
244 verts = [v.index for v in bm.verts if v.select]
245 verts_orig = props.verts_orig
247 # move UV followed by vertex coordinate
248 for vidx, v_orig in zip(verts, verts_orig):
249 if vidx != v_orig["vidx"]:
250 self.report({'ERROR'}, "Internal Error")
251 return {"CANCELLED"}
253 v = bm.verts[vidx]
254 link_loops = get_link_loops(v)
256 result = []
258 for ll in link_loops:
259 ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
260 target_uv = get_target_uv(
261 ll, uv_layer, verts_orig, v, ini_geom)
262 result.append({"l": ll["l"], "uv": target_uv})
264 # connect other face's UV
265 if self.connect:
266 ave = Vector((0.0, 0.0))
267 for r in result:
268 ave = ave + r["uv"]
269 ave = ave / len(result)
270 for r in result:
271 r["l"][uv_layer].uv = ave
272 else:
273 for r in result:
274 r["l"][uv_layer].uv = r["uv"]
275 v_orig["moved"] = True
276 bmesh.update_edit_mesh(obj.data)
278 return {'FINISHED'}
281 class MUV_TexLockUpdater(bpy.types.Operator):
283 Operation class: Texture locking updater
286 bl_idname = "uv.muv_texlock_updater"
287 bl_label = "Texture Lock Updater"
288 bl_description = "Texture Lock Updater"
290 def __init__(self):
291 self.__timer = None
293 def __update_uv(self, context):
295 Update UV when vertex coordinates are changed
297 props = context.scene.muv_props.texlock
298 obj = bpy.context.active_object
299 bm = bmesh.from_edit_mesh(obj.data)
300 if common.check_version(2, 73, 0) >= 0:
301 bm.verts.ensure_lookup_table()
302 bm.edges.ensure_lookup_table()
303 bm.faces.ensure_lookup_table()
305 if not bm.loops.layers.uv:
306 self.report({'WARNING'}, "Object must have more than one UV map")
307 return
308 uv_layer = bm.loops.layers.uv.verify()
310 verts = [v.index for v in bm.verts if v.select]
311 verts_orig = props.intr_verts_orig
313 for vidx, v_orig in zip(verts, verts_orig):
314 if vidx != v_orig["vidx"]:
315 self.report({'ERROR'}, "Internal Error")
316 return
318 v = bm.verts[vidx]
319 link_loops = get_link_loops(v)
321 result = []
322 for ll in link_loops:
323 ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
324 target_uv = get_target_uv(
325 ll, uv_layer, verts_orig, v, ini_geom)
326 result.append({"l": ll["l"], "uv": target_uv})
328 # UV connect option is always true, because it raises
329 # unexpected behavior
330 ave = Vector((0.0, 0.0))
331 for r in result:
332 ave = ave + r["uv"]
333 ave = ave / len(result)
334 for r in result:
335 r["l"][uv_layer].uv = ave
336 v_orig["moved"] = True
337 bmesh.update_edit_mesh(obj.data)
339 common.redraw_all_areas()
340 props.intr_verts_orig = [
341 {"vidx": v.index, "vco": v.co.copy(), "moved": False}
342 for v in bm.verts if v.select]
344 def modal(self, context, event):
345 props = context.scene.muv_props.texlock
346 if context.area:
347 context.area.tag_redraw()
348 if props.intr_running is False:
349 self.__handle_remove(context)
350 return {'FINISHED'}
351 if event.type == 'TIMER':
352 self.__update_uv(context)
354 return {'PASS_THROUGH'}
356 def __handle_add(self, context):
357 if self.__timer is None:
358 self.__timer = context.window_manager.event_timer_add(
359 0.10, context.window)
360 context.window_manager.modal_handler_add(self)
362 def __handle_remove(self, context):
363 if self.__timer is not None:
364 context.window_manager.event_timer_remove(self.__timer)
365 self.__timer = None
367 def execute(self, context):
368 props = context.scene.muv_props.texlock
369 if props.intr_running is False:
370 self.__handle_add(context)
371 props.intr_running = True
372 return {'RUNNING_MODAL'}
373 else:
374 props.intr_running = False
375 if context.area:
376 context.area.tag_redraw()
378 return {'FINISHED'}
381 class MUV_TexLockIntrStart(bpy.types.Operator):
383 Operation class: Start texture locking (Interactive mode)
386 bl_idname = "uv.muv_texlock_intr_start"
387 bl_label = "Texture Lock Start (Interactive mode)"
388 bl_description = "Texture Lock Start (Realtime UV update)"
389 bl_options = {'REGISTER', 'UNDO'}
391 def execute(self, context):
392 props = context.scene.muv_props.texlock
393 if props.intr_running is True:
394 return {'CANCELLED'}
396 obj = bpy.context.active_object
397 bm = bmesh.from_edit_mesh(obj.data)
398 if common.check_version(2, 73, 0) >= 0:
399 bm.verts.ensure_lookup_table()
400 bm.edges.ensure_lookup_table()
401 bm.faces.ensure_lookup_table()
403 if not bm.loops.layers.uv:
404 self.report({'WARNING'}, "Object must have more than one UV map")
405 return {'CANCELLED'}
407 props.intr_verts_orig = [
408 {"vidx": v.index, "vco": v.co.copy(), "moved": False}
409 for v in bm.verts if v.select]
411 bpy.ops.uv.muv_texlock_updater()
413 return {'FINISHED'}
416 # Texture lock (Stop, Interactive mode)
417 class MUV_TexLockIntrStop(bpy.types.Operator):
419 Operation class: Stop texture locking (interactive mode)
422 bl_idname = "uv.muv_texlock_intr_stop"
423 bl_label = "Texture Lock Stop (Interactive mode)"
424 bl_description = "Texture Lock Stop (Realtime UV update)"
425 bl_options = {'REGISTER', 'UNDO'}
427 def execute(self, context):
428 props = context.scene.muv_props.texlock
429 if props.intr_running is False:
430 return {'CANCELLED'}
432 bpy.ops.uv.muv_texlock_updater()
434 return {'FINISHED'}