├── .gitignore ├── DUV_ApplyMaterial.py ├── DUV_HotSpot.py ├── DUV_UVBoxmap.py ├── DUV_UVCopy.py ├── DUV_UVCycle.py ├── DUV_UVExtend.py ├── DUV_UVInset.py ├── DUV_UVMirror.py ├── DUV_UVMoveToEdge.py ├── DUV_UVProject.py ├── DUV_UVRotate.py ├── DUV_UVScale.py ├── DUV_UVStitch.py ├── DUV_UVTransfer.py ├── DUV_UVTranslate.py ├── DUV_UVTrim.py ├── DUV_UVUnwrap.py ├── DUV_Utils.py ├── DUV_Utils_backup.py ├── README.md ├── __init__.py └── example_atlas.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Unit test / coverage reports 7 | htmlcov/ 8 | .tox/ 9 | .nox/ 10 | .coverage 11 | .coverage.* 12 | .cache 13 | nosetests.xml 14 | coverage.xml 15 | *.cover 16 | *.py,cover 17 | .hypothesis/ 18 | .pytest_cache/ 19 | 20 | # Environments 21 | .env 22 | .venv 23 | env/ 24 | venv/ 25 | ENV/ 26 | env.bak/ 27 | venv.bak/ 28 | 29 | # Pyre type checker 30 | .pyre/ -------------------------------------------------------------------------------- /DUV_ApplyMaterial.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | from mathutils import Vector 5 | from . import DUV_Utils 6 | 7 | 8 | class DREAMUV_OT_apply_material(bpy.types.Operator): 9 | """Unwrap and attempt to fit to a square shape""" 10 | bl_idname = "view3d.dreamuv_apply_material" 11 | bl_label = "unwrap to square shape if possible" 12 | 13 | def execute(self, context): 14 | 15 | #check for object or edit mode: 16 | objectmode = False 17 | if bpy.context.object.mode == 'OBJECT': 18 | objectmode = True 19 | #switch to edit and select all 20 | bpy.ops.object.editmode_toggle() 21 | bpy.ops.mesh.select_all(action='SELECT') 22 | 23 | obj = bpy.context.view_layer.objects.active 24 | bm = bmesh.from_edit_mesh(obj.data) 25 | uv_layer = bm.loops.layers.uv.verify() 26 | 27 | HSfaces = list() 28 | #MAKE FACE LIST 29 | for face in bm.faces: 30 | if face.select: 31 | HSfaces.append(face) 32 | 33 | #ADD MATERIAL 34 | #check if we want to add material, then check if it needs to be added, and keep index for later 35 | if context.scene.duv_hotspotmaterial is not None: 36 | matindex = 0 37 | doesmatexist = False 38 | for m in obj.data.materials: 39 | if m == context.scene.duv_hotspotmaterial: 40 | doesmatexist = True 41 | break 42 | matindex += 1 43 | if doesmatexist is False: 44 | obj.data.materials.append(context.scene.duv_hotspotmaterial) 45 | 46 | 47 | #apply material from index 48 | if context.scene.duv_hotspotmaterial is not None: 49 | for face in HSfaces: 50 | face.material_index = matindex 51 | 52 | bmesh.update_edit_mesh(obj.data) 53 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 54 | 55 | if objectmode is True: 56 | bpy.ops.mesh.select_all(action='DESELECT') 57 | bpy.ops.object.editmode_toggle() 58 | 59 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_HotSpot.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | import random 5 | from mathutils import Vector 6 | from . import DUV_Utils 7 | from bpy.props import EnumProperty, BoolProperty, StringProperty, FloatProperty, IntProperty 8 | 9 | 10 | def main(context): 11 | #Check if an atlas object exists 12 | if context.scene.subrect_atlas is None: 13 | self.report({'WARNING'}, "No valid atlas selected!") 14 | return {'FINISHED'} 15 | 16 | #make sure active object is actually selected in edit mode: 17 | if bpy.context.object.mode == 'EDIT': 18 | bpy.context.object.select_set(True) 19 | 20 | 21 | #check for object or edit mode: 22 | objectmode = False 23 | if bpy.context.object.mode == 'OBJECT': 24 | objectmode = True 25 | #switch to edit and select all 26 | bpy.ops.object.editmode_toggle() 27 | bpy.ops.mesh.select_all(action='SELECT') 28 | 29 | #check if uv sync selection is used and turn off if so 30 | uvsync = False 31 | if bpy.context.scene.tool_settings.use_uv_select_sync == True: 32 | uvsync = True 33 | bpy.context.scene.tool_settings.use_uv_select_sync = False 34 | 35 | 36 | obj = bpy.context.view_layer.objects.active 37 | bm = bmesh.from_edit_mesh(obj.data) 38 | 39 | #ADD MATERIAL 40 | if context.scene.duv_hotspotmaterial is not None: 41 | matindex = 0 42 | doesmatexist = False 43 | for m in obj.data.materials: 44 | if m == context.scene.duv_hotspotmaterial: 45 | doesmatexist = True 46 | break 47 | matindex += 1 48 | if doesmatexist is False: 49 | obj.data.materials.append(context.scene.duv_hotspotmaterial) 50 | for face in bm.faces: 51 | if face.select: 52 | face.material_index = matindex 53 | bmesh.update_edit_mesh(obj.data) 54 | 55 | #CREATE WORKING DUPLICATE! 56 | object_original = bpy.context.view_layer.objects.active 57 | bpy.ops.object.editmode_toggle() 58 | bpy.ops.object.duplicate() 59 | 60 | #setup hard edges on duplicate 61 | #create hard edges 62 | 63 | 64 | #bpy.ops.object.shade_smooth_by_angle() 65 | 66 | smoothmodifier = False 67 | for m in bpy.context.active_object.modifiers: 68 | if m.name == 'Auto Smooth' or m.name == 'Smooth by Angle': 69 | smoothmodifier = True 70 | 71 | if smoothmodifier: 72 | #apply smoothing modifier 73 | bpy.ops.object.modifier_apply(modifier="Smooth by Angle") 74 | else: 75 | #auto smooth - assume 30 degrees until someone complains 76 | bpy.ops.object.shade_smooth_by_angle(angle=0.523599) 77 | 78 | 79 | bpy.ops.object.editmode_toggle() 80 | bpy.context.view_layer.objects.active.name = "dreamuv_temp" 81 | object_temporary = bpy.context.view_layer.objects.active 82 | 83 | #PREPROCESS - save seams and hard edges 84 | obj = bpy.context.view_layer.objects.active 85 | bm = bmesh.from_edit_mesh(obj.data) 86 | 87 | faces = list() 88 | for face in bm.faces: 89 | if face.select: 90 | faces.append(face) 91 | 92 | bmesh.update_edit_mesh(obj.data) 93 | bpy.ops.mesh.select_all(action='DESELECT') 94 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') 95 | 96 | #broken in 4.1 97 | #angle = bpy.context.object.data.auto_smooth_angle 98 | #bpy.ops.mesh.edges_select_sharp(sharpness=angle) 99 | 100 | 101 | bpy.ops.mesh.mark_seam(clear=False) 102 | bpy.ops.mesh.select_all(action='DESELECT') 103 | 104 | for edge in bm.edges: 105 | if edge.seam or edge.smooth == False: 106 | edge.select = True 107 | 108 | bpy.ops.mesh.edge_split(type='EDGE') 109 | bpy.ops.mesh.select_all(action='DESELECT') 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | #select all faces to be hotspotted again: 121 | 122 | for face in faces: 123 | face.select = True 124 | 125 | #PREPROCESS - find islands 126 | 127 | #create UV islands using blender unwrap 128 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=1.0) 129 | #list islands 130 | #iterate using select linked uv 131 | 132 | islands = list() 133 | tempfaces = list() 134 | updatedfaces = list() 135 | #MAKE FACE LIST 136 | for face in bm.faces: 137 | if face.select: 138 | updatedfaces.append(face) 139 | tempfaces.append(face) 140 | face.select = False 141 | 142 | while len(tempfaces) > 0: 143 | updatedfaces[0].select = True 144 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 145 | bpy.ops.mesh.select_linked(delimit={'UV'}) 146 | 147 | islandfaces = list() 148 | for face in bm.faces: 149 | if face.select: 150 | islandfaces.append(face) 151 | islands.append(islandfaces) 152 | 153 | #create updated list 154 | tempfaces.clear() 155 | for face in updatedfaces: 156 | if face.select == False: 157 | tempfaces.append(face) 158 | else: 159 | face.select = False 160 | #make new list into updated list 161 | updatedfaces.clear() 162 | updatedfaces = tempfaces.copy() 163 | 164 | bpy.ops.uv.select_all(action='SELECT') 165 | 166 | #get atlas 167 | atlas = DUV_Utils.read_atlas(context) 168 | 169 | #NOW ITERATE! 170 | for island in islands: 171 | uv_layer = bm.loops.layers.uv.verify() 172 | 173 | for face in faces: 174 | face.select = False 175 | for face in island: 176 | face.select = True 177 | 178 | HSfaces = list() 179 | #MAKE FACE LIST 180 | for face in bm.faces: 181 | if face.select: 182 | HSfaces.append(face) 183 | 184 | #get original size 185 | xmin2, xmax2 = HSfaces[0].loops[0][uv_layer].uv.x, HSfaces[0].loops[0][uv_layer].uv.x 186 | ymin2, ymax2 = HSfaces[0].loops[0][uv_layer].uv.y, HSfaces[0].loops[0][uv_layer].uv.y 187 | for face in HSfaces: 188 | for vert in face.loops: 189 | xmin2 = min(xmin2, vert[uv_layer].uv.x) 190 | xmax2 = max(xmax2, vert[uv_layer].uv.x) 191 | ymin2 = min(ymin2, vert[uv_layer].uv.y) 192 | ymax2 = max(ymax2, vert[uv_layer].uv.y) 193 | 194 | #try fitting selection to square 195 | is_rect = DUV_Utils.square_fit(context) 196 | if is_rect is False: 197 | 198 | #return {'FINISHED'} 199 | 200 | bmesh.update_edit_mesh(obj.data) 201 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 202 | uv_layer = bm.loops.layers.uv.verify() 203 | 204 | #rotate to world angle here: 205 | DUV_Utils.get_orientation(context) 206 | 207 | #FIT TO 0-1 range 208 | xmin, xmax = HSfaces[0].loops[0][uv_layer].uv.x, HSfaces[0].loops[0][uv_layer].uv.x 209 | ymin, ymax = HSfaces[0].loops[0][uv_layer].uv.y, HSfaces[0].loops[0][uv_layer].uv.y 210 | 211 | for face in HSfaces: 212 | for vert in face.loops: 213 | xmin = min(xmin, vert[uv_layer].uv.x) 214 | xmax = max(xmax, vert[uv_layer].uv.x) 215 | ymin = min(ymin, vert[uv_layer].uv.y) 216 | ymax = max(ymax, vert[uv_layer].uv.y) 217 | 218 | #prevent divide by 0: 219 | if (xmax - xmin) == 0: 220 | xmin = .1 221 | if (ymax - ymin) == 0: 222 | ymin = .1 223 | 224 | for face in HSfaces: 225 | for loop in face.loops: 226 | loop[uv_layer].uv.x -= xmin 227 | loop[uv_layer].uv.y -= ymin 228 | loop[uv_layer].uv.x /= (xmax-xmin) 229 | loop[uv_layer].uv.y /= (ymax-ymin) 230 | 231 | edge1 = xmax-xmin 232 | edge2 = ymax-ymin 233 | aspect = edge1/edge2 234 | size = area = sum(f.calc_area() for f in HSfaces if f.select) 235 | 236 | if is_rect is False: 237 | #calulate ratio empty vs full 238 | sizeratio = DUV_Utils.get_uv_ratio(context) 239 | #prevent divide by 0: 240 | if sizeratio == 0: 241 | sizeratio = 1.0 242 | size = size / sizeratio 243 | 244 | if aspect > 1: 245 | aspect = round(aspect) 246 | else: 247 | if aspect > 0.0001: #prevent divide by 0 248 | aspect = 1/(round(1/aspect)) 249 | 250 | #ASPECT LOWER THAN 1.0 = TALL 251 | #ASPECT HIGHER THAN 1.0 = WIDE 252 | 253 | #find closest aspect ratio in list 254 | 255 | #2 variations depending on tall or wide 256 | 257 | index = 0 258 | templength = abs(atlas[0].posaspect-aspect) 259 | tempindex = 0 260 | 261 | worldorientation = context.scene.duv_useorientation 262 | 263 | if worldorientation: 264 | for number in atlas: 265 | testlength = abs(number.aspect-aspect) 266 | if testlength < templength: 267 | templength = testlength 268 | tempindex = index 269 | index += 1 270 | 271 | if not worldorientation: 272 | 273 | #wide: 274 | if aspect >= 1.0: 275 | for number in atlas: 276 | testlength = abs(number.posaspect-aspect) 277 | if testlength < templength: 278 | templength = testlength 279 | tempindex = index 280 | index += 1 281 | 282 | #tall: 283 | if aspect < 1.0: 284 | templength = abs((atlas[0].posaspect)-(1/aspect)) 285 | for number in atlas: 286 | testlength = abs((number.posaspect)-(1/aspect)) 287 | if testlength < templength: 288 | templength = testlength 289 | tempindex = index 290 | index += 1 291 | 292 | #NOW MAKE LIST OF ASPECTS! 293 | flipped = False 294 | aspectbucket = list() 295 | for r in atlas: 296 | if r.aspect == atlas[tempindex].aspect: 297 | aspectbucket.append(r) 298 | if worldorientation is False: 299 | if r.aspect == 1/atlas[tempindex].aspect: 300 | aspectbucket.append(r) 301 | 302 | #find closest size in bucket: 303 | index = 0 304 | 305 | templength = abs(aspectbucket[0].size-size) 306 | tempindex = 0 307 | 308 | validrects = list() 309 | for a in aspectbucket: 310 | testlength = abs(a.size-size) 311 | if testlength <= templength: 312 | templength = testlength 313 | tempindex = index 314 | index += 1 315 | 316 | index = 0 317 | for a in aspectbucket: 318 | if a.size == aspectbucket[tempindex].size: 319 | validrects.append(index) 320 | index += 1 321 | 322 | tempindex = random.choice(validrects) 323 | 324 | #test if coords are already asigned by comparing minmaxes, then try again 325 | 326 | #2 assign uv 327 | #get minmax of target rect 328 | xmin, xmax = aspectbucket[tempindex].uvcoord[0].x, aspectbucket[tempindex].uvcoord[0].x 329 | ymin, ymax = aspectbucket[tempindex].uvcoord[0].y, aspectbucket[tempindex].uvcoord[0].y 330 | 331 | for vert in aspectbucket[tempindex].uvcoord: 332 | 333 | xmin = min(xmin, vert.x) 334 | xmax = max(xmax, vert.x) 335 | ymin = min(ymin, vert.y) 336 | ymax = max(ymax, vert.y) 337 | 338 | #flip if aspect is inverted 339 | 340 | if xmin == xmin2 and ymin == ymin2 and xmax == xmax2 and ymax == ymax2 and len(validrects) > 1: 341 | #remove current choice 342 | validrects.remove(tempindex) 343 | #print(validrects) 344 | 345 | tempindex = random.choice(validrects) 346 | 347 | xmin, xmax = aspectbucket[tempindex].uvcoord[0].x, aspectbucket[tempindex].uvcoord[0].x 348 | ymin, ymax = aspectbucket[tempindex].uvcoord[0].y, aspectbucket[tempindex].uvcoord[0].y 349 | 350 | for vert in aspectbucket[tempindex].uvcoord: 351 | xmin = min(xmin, vert.x) 352 | xmax = max(xmax, vert.x) 353 | ymin = min(ymin, vert.y) 354 | ymax = max(ymax, vert.y) 355 | 356 | #flip U and V if aspect is reversed: 357 | #WIDE case becomes TALL 358 | if aspectbucket[tempindex].aspect < 1.0 and aspect >= 1.0: 359 | for face in HSfaces: 360 | for loop in face.loops: 361 | newx = loop[uv_layer].uv.y 362 | newy = loop[uv_layer].uv.x 363 | loop[uv_layer].uv.x = newx 364 | loop[uv_layer].uv.y = newy 365 | 366 | #TALL case becomes WIDE 367 | if aspectbucket[tempindex].aspect > 1.0 and aspect < 1.0: 368 | for face in HSfaces: 369 | for loop in face.loops: 370 | newx = loop[uv_layer].uv.y 371 | newy = loop[uv_layer].uv.x 372 | loop[uv_layer].uv.x = newx 373 | loop[uv_layer].uv.y = newy 374 | 375 | #check if uv needs to be inset 376 | if context.scene.duv_hotspotuseinset is True: 377 | pixel_inset = context.scene.hotspotinsetpixels/context.scene.hotspotinsettexsize 378 | xmin += pixel_inset 379 | xmax -= pixel_inset 380 | ymin += pixel_inset 381 | ymax -= pixel_inset 382 | 383 | #apply the new UV 384 | for face in HSfaces: 385 | for loop in face.loops: 386 | loop[uv_layer].uv.x *= xmax-xmin 387 | loop[uv_layer].uv.y *= ymax-ymin 388 | loop[uv_layer].uv.x += xmin 389 | loop[uv_layer].uv.y += ymin 390 | 391 | worldorientation = context.scene.duv_useorientation 392 | use_mirrorx = context.scene.duv_usemirrorx 393 | use_mirrory = context.scene.duv_usemirrory 394 | 395 | #MIRRORING: 396 | 397 | if worldorientation is False: 398 | #flip around square aspects randomly 399 | if aspect == 1: 400 | flips = random.randint(0, 3) 401 | for x in range(flips): 402 | bpy.ops.view3d.dreamuv_uvcycle() 403 | 404 | #and also do randomized mirroring: 405 | if use_mirrorx is True: 406 | randomMirrorX = random.randint(0, 1) 407 | if randomMirrorX == 1: 408 | op = bpy.ops.view3d.dreamuv_uvmirror(direction = "x") 409 | 410 | if use_mirrory is True: 411 | randomMirrorY = random.randint(0, 1) 412 | if randomMirrorY == 1: 413 | op = bpy.ops.view3d.dreamuv_uvmirror(direction = "y") 414 | 415 | #apply material from index 416 | if context.scene.duv_hotspotmaterial is not None: 417 | for face in HSfaces: 418 | face.material_index = matindex 419 | 420 | for face in faces: 421 | face.select = True 422 | bmesh.update_edit_mesh(obj.data) 423 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 424 | 425 | #transfer UV maps back to original mesh 426 | 427 | obj = bpy.context.view_layer.objects.active 428 | bm = bmesh.from_edit_mesh(obj.data) 429 | uv_layer = bm.loops.layers.uv.verify() 430 | uv_backup = list(); 431 | #print("new UV:") 432 | for face in bm.faces: 433 | backupface = list() 434 | for vert in face.loops: 435 | backupuv = list() 436 | backupuv.append(vert[uv_layer].uv.x) 437 | backupuv.append(vert[uv_layer].uv.y) 438 | backupface.append(backupuv) 439 | #print(backupuv) 440 | uv_backup.append(backupface) 441 | 442 | #now apply to original mesh 443 | bpy.ops.object.editmode_toggle() 444 | object_temporary.select_set(False) 445 | object_original.select_set(True) 446 | bpy.ops.object.editmode_toggle() 447 | 448 | obj = object_original 449 | bm = bmesh.from_edit_mesh(obj.data) 450 | uv_layer = bm.loops.layers.uv.verify() 451 | #uv_backup = list(); 452 | #print("new UV:") 453 | for face, backupface in zip(bm.faces, uv_backup): 454 | for vert, backupuv in zip(face.loops, backupface): 455 | vert[uv_layer].uv.x = backupuv[0] 456 | vert[uv_layer].uv.y = backupuv[1] 457 | bmesh.update_edit_mesh(obj.data) 458 | 459 | 460 | 461 | bpy.ops.object.editmode_toggle() 462 | 463 | object_original.select_set(False) 464 | object_temporary.select_set(True) 465 | bpy.ops.object.delete(use_global=False) 466 | object_original.select_set(True) 467 | context.view_layer.objects.active=bpy.context.selected_objects[0] 468 | 469 | if uvsync == True: 470 | bpy.ops.object.editmode_toggle() 471 | bpy.context.scene.tool_settings.use_uv_select_sync = True 472 | bpy.ops.object.editmode_toggle() 473 | 474 | 475 | if objectmode is False: 476 | bpy.ops.object.editmode_toggle() 477 | 478 | #temp - do both uvs! 479 | #if context.scene.duv_uv2copy == True: 480 | # if bpy.context.object.mode == 'EDIT': 481 | # bpy.ops.brm.copyuvs() 482 | # print("copying uvs") 483 | 484 | class DREAMUV_OT_hotspotter(bpy.types.Operator): 485 | """Unwrap selection using the atlas object as a guide""" 486 | bl_idname = "view3d.dreamuv_hotspotter" 487 | bl_label = "HotSpot" 488 | bl_options = {"UNDO"} 489 | 490 | def execute(self, context): 491 | 492 | #make sure selection is active: 493 | if context.scene.duv_hotspot_atlas1 == True: 494 | context.scene.subrect_atlas = context.scene.subrect_atlas1 495 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial1 496 | if context.scene.duv_hotspot_atlas2 == True: 497 | context.scene.subrect_atlas = context.scene.subrect_atlas2 498 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial2 499 | if context.scene.duv_hotspot_atlas3 == True: 500 | context.scene.subrect_atlas = context.scene.subrect_atlas3 501 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial3 502 | if context.scene.duv_hotspot_atlas4 == True: 503 | context.scene.subrect_atlas = context.scene.subrect_atlas4 504 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial4 505 | if context.scene.duv_hotspot_atlas5 == True: 506 | context.scene.subrect_atlas = context.scene.subrect_atlas5 507 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial5 508 | if context.scene.duv_hotspot_atlas6 == True: 509 | context.scene.subrect_atlas = context.scene.subrect_atlas6 510 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial6 511 | if context.scene.duv_hotspot_atlas7 == True: 512 | context.scene.subrect_atlas = context.scene.subrect_atlas7 513 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial7 514 | if context.scene.duv_hotspot_atlas8 == True: 515 | context.scene.subrect_atlas = context.scene.subrect_atlas8 516 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial8 517 | 518 | 519 | #remember selected uv 520 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 521 | if context.scene.duv_hotspot_uv1 == True: 522 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 523 | main(context) 524 | if context.scene.duv_hotspot_uv2 == True: 525 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 526 | main(context) 527 | if context.scene.duv_hotspot_uv1 == False and context.scene.duv_hotspot_uv2 == False: 528 | #just uv selected uv 529 | main(context) 530 | #reset selected uv 531 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 532 | 533 | if context.scene.duv_autoboxmap == True: 534 | bpy.ops.view3d.dreamuv_uvboxmap() 535 | 536 | #main(context) 537 | return {'FINISHED'} 538 | 539 | bpy.ops.object.editmode_toggle() 540 | 541 | class DREAMUV_OT_pushhotspot(bpy.types.Operator): 542 | """Set hotspot settings from list""" 543 | bl_idname = "view3d.dreamuv_pushhotspot" 544 | bl_label = "Push HotSpot" 545 | bl_options = {"UNDO"} 546 | 547 | index : bpy.props.IntProperty() 548 | 549 | def execute(self, context): 550 | 551 | context.scene.duv_hotspot_atlas1 = False 552 | context.scene.duv_hotspot_atlas2 = False 553 | context.scene.duv_hotspot_atlas3 = False 554 | context.scene.duv_hotspot_atlas4 = False 555 | context.scene.duv_hotspot_atlas5 = False 556 | context.scene.duv_hotspot_atlas6 = False 557 | context.scene.duv_hotspot_atlas7 = False 558 | context.scene.duv_hotspot_atlas8 = False 559 | 560 | if self.index == 1: 561 | context.scene.subrect_atlas = context.scene.subrect_atlas1 562 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial1 563 | context.scene.duv_hotspot_atlas1 = True 564 | if self.index == 2: 565 | context.scene.subrect_atlas = context.scene.subrect_atlas2 566 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial2 567 | context.scene.duv_hotspot_atlas2 = True 568 | if self.index == 3: 569 | context.scene.subrect_atlas = context.scene.subrect_atlas3 570 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial3 571 | context.scene.duv_hotspot_atlas3 = True 572 | if self.index == 4: 573 | context.scene.subrect_atlas = context.scene.subrect_atlas4 574 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial4 575 | context.scene.duv_hotspot_atlas4 = True 576 | if self.index == 5: 577 | context.scene.subrect_atlas = context.scene.subrect_atlas5 578 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial5 579 | context.scene.duv_hotspot_atlas5 = True 580 | if self.index == 6: 581 | context.scene.subrect_atlas = context.scene.subrect_atlas6 582 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial6 583 | context.scene.duv_hotspot_atlas6 = True 584 | if self.index == 7: 585 | context.scene.subrect_atlas = context.scene.subrect_atlas7 586 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial7 587 | context.scene.duv_hotspot_atlas7 = True 588 | if self.index == 8: 589 | context.scene.subrect_atlas = context.scene.subrect_atlas8 590 | context.scene.duv_hotspotmaterial = context.scene.duv_hotspotmaterial8 591 | context.scene.duv_hotspot_atlas8 = True 592 | 593 | return {'FINISHED'} 594 | -------------------------------------------------------------------------------- /DUV_UVBoxmap.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from math import degrees 4 | from math import radians 5 | from mathutils import Vector 6 | from . import DUV_Utils 7 | 8 | 9 | def main(context): 10 | 11 | #Check if a box object exists 12 | if context.scene.uv_box is None: 13 | self.report({'WARNING'}, "No valid box reference assigned!") 14 | return {'FINISHED'} 15 | 16 | #make sure active object is actually selected in edit mode: 17 | if bpy.context.object.mode == 'EDIT': 18 | bpy.context.object.select_set(True) 19 | 20 | 21 | #check for object or edit mode: 22 | objectmode = False 23 | if bpy.context.object.mode == 'OBJECT': 24 | objectmode = True 25 | print("switch to edit mode") 26 | #switch to edit and select all 27 | bpy.ops.object.editmode_toggle() 28 | bpy.ops.mesh.select_all(action='SELECT') 29 | 30 | 31 | 32 | obj = context.scene.uv_box 33 | 34 | mat = obj.matrix_world 35 | 36 | startloc = mat @ obj.bound_box.data.data.vertices[0].co 37 | 38 | xmin = startloc.x 39 | xmax = startloc.x 40 | ymin = startloc.y 41 | ymax = startloc.y 42 | zmin = startloc.z 43 | zmax = startloc.z 44 | 45 | for v in obj.bound_box.data.data.vertices: 46 | 47 | loc = mat @ v.co 48 | 49 | if loc.x < xmin: 50 | xmin=loc.x 51 | elif loc.x >= xmax: 52 | xmax=loc.x 53 | 54 | if loc.y < ymin: 55 | ymin=loc.y 56 | elif loc.y >= ymax: 57 | ymax=loc.y 58 | 59 | if loc.z < zmin: 60 | zmin=loc.z 61 | elif loc.z >= zmax: 62 | zmax=loc.z 63 | 64 | up = Vector((0, 0, 1)) # -z axis. 65 | right = Vector((1, 0, 0)) # -x axis. 66 | front = Vector((0, 1, 0)) # -y axis. 67 | test_angle = radians(89) 68 | 69 | 70 | obj = bpy.context.view_layer.objects.active 71 | bm = bmesh.from_edit_mesh(obj.data) 72 | #uv_layer = bm.loops.layers.uv[0] 73 | uv_layer = bm.loops.layers.uv.verify() 74 | mat = obj.matrix_world 75 | 76 | for f in bm.faces: 77 | if f.select: 78 | 79 | #print(degrees(f.normal.angle(up))) 80 | 81 | m = obj.matrix_world.to_quaternion().to_matrix() 82 | 83 | f_world_normal = m @ f.normal 84 | 85 | #gather up, front and right angles 86 | 87 | upangle = degrees(f_world_normal.angle(up)) 88 | if upangle > 90: 89 | upangle = 180 - upangle 90 | rightangle = degrees(f_world_normal.angle(right)) 91 | if rightangle > 90: 92 | rightangle = 180 - rightangle 93 | frontangle = degrees(f_world_normal.angle(front)) 94 | if frontangle > 90: 95 | frontangle = 180 - frontangle 96 | 97 | #pick smallest angle 98 | 99 | if upangle <= rightangle and upangle <= frontangle: 100 | for loop in f.loops: 101 | vco = loop.vert.co 102 | # Multiply matrix by vertex (see also: https://developer.blender.org/T56276) 103 | loc = mat @ vco 104 | loop[uv_layer].uv.x = loc.x 105 | loop[uv_layer].uv.y = loc.y 106 | loop[uv_layer].uv.x -= xmin 107 | loop[uv_layer].uv.y -= ymin 108 | loop[uv_layer].uv.x /= (xmax-xmin) 109 | loop[uv_layer].uv.y /= (ymax-ymin) 110 | 111 | elif rightangle <= upangle and rightangle <= frontangle: 112 | for loop in f.loops: 113 | vco = loop.vert.co 114 | # Multiply matrix by vertex (see also: https://developer.blender.org/T56276) 115 | loc = mat @ vco 116 | loop[uv_layer].uv.x = loc.y 117 | loop[uv_layer].uv.y = loc.z 118 | loop[uv_layer].uv.x -= ymin 119 | loop[uv_layer].uv.y -= zmin 120 | loop[uv_layer].uv.x /= (ymax-ymin) 121 | loop[uv_layer].uv.y /= (zmax-zmin) 122 | 123 | elif frontangle <= upangle and frontangle <= rightangle: 124 | for loop in f.loops: 125 | vco = loop.vert.co 126 | # Multiply matrix by vertex (see also: https://developer.blender.org/T56276) 127 | loc = mat @ vco 128 | loop[uv_layer].uv.x = loc.x 129 | loop[uv_layer].uv.y = loc.z 130 | loop[uv_layer].uv.x -= xmin 131 | loop[uv_layer].uv.y -= zmin 132 | loop[uv_layer].uv.x /= (xmax-xmin) 133 | loop[uv_layer].uv.y /= (zmax-zmin) 134 | 135 | #backup uv just in case 136 | else: 137 | for loop in f.loops: 138 | vco = loop.vert.co 139 | # Multiply matrix by vertex (see also: https://developer.blender.org/T56276) 140 | loc = mat @ vco 141 | loop[uv_layer].uv.x = loc.x 142 | loop[uv_layer].uv.y = loc.z 143 | loop[uv_layer].uv.x -= xmin 144 | loop[uv_layer].uv.y -= zmin 145 | loop[uv_layer].uv.x /= (xmax-xmin) 146 | loop[uv_layer].uv.y /= (zmax-zmin) 147 | 148 | 149 | bmesh.update_edit_mesh(obj.data) 150 | 151 | if objectmode is True: 152 | print("switch to object mode") 153 | bpy.ops.object.editmode_toggle() 154 | 155 | class DREAMUV_OT_uv_boxmap(bpy.types.Operator): 156 | """Unwrap using a box shape""" 157 | bl_idname = "view3d.dreamuv_uvboxmap" 158 | bl_label = "box map" 159 | 160 | def execute(self, context): 161 | #remember selected uv 162 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 163 | if context.scene.duv_boxmap_uv1 == True: 164 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 165 | main(context) 166 | if context.scene.duv_boxmap_uv2 == True: 167 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 168 | main(context) 169 | if context.scene.duv_boxmap_uv1 == False and context.scene.duv_boxmap_uv2 == False: 170 | #just uv selected uv 171 | main(context) 172 | #reset selected uv 173 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 174 | 175 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVCopy.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | 4 | class DREAMUV_OT_uv_copy(bpy.types.Operator): 5 | """Copy selected UVs to other UV set""" 6 | bl_idname = "view3d.dreamuv_uvcopy" 7 | bl_label = "copy selected uvs from uv2 to uv1" 8 | bl_options = {"UNDO"} 9 | #bl_options = {'SEARCH_ON_KEY_PRESS'} 10 | 11 | reverse : bpy.props.BoolProperty() 12 | 13 | def execute(self, context): 14 | if len(bpy.context.object.data.uv_layers) == 2: 15 | 16 | #make sure active object is actually selected in edit mode: 17 | if bpy.context.object.mode == 'EDIT': 18 | bpy.context.object.select_set(True) 19 | 20 | #check for object or edit mode: 21 | objectmode = False 22 | if bpy.context.object.mode == 'OBJECT': 23 | objectmode = True 24 | #switch to edit and select all 25 | bpy.ops.object.editmode_toggle() 26 | bpy.ops.mesh.select_all(action='SELECT') 27 | 28 | 29 | obj = bpy.context.view_layer.objects.active 30 | bm = bmesh.from_edit_mesh(obj.data) 31 | uv_layer = bm.loops.layers.uv[0] 32 | uv_layer2 = bm.loops.layers.uv[1] 33 | 34 | for f in bm.faces: 35 | if f.select: 36 | for vert in f.loops: 37 | if self.reverse == True: 38 | vert[uv_layer].uv.x = vert[uv_layer2].uv.x 39 | vert[uv_layer].uv.y = vert[uv_layer2].uv.y 40 | if self.reverse == False: 41 | vert[uv_layer2].uv.x = vert[uv_layer].uv.x 42 | vert[uv_layer2].uv.y = vert[uv_layer].uv.y 43 | 44 | bmesh.update_edit_mesh(obj.data) 45 | 46 | if objectmode is True: 47 | bpy.ops.object.editmode_toggle() 48 | 49 | return {'FINISHED'} 50 | -------------------------------------------------------------------------------- /DUV_UVCycle.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import bmesh 4 | from mathutils import Vector 5 | 6 | class DREAMUV_OT_uv_cycle(bpy.types.Operator): 7 | """Rotate UVs but retain uv edge positions""" 8 | bl_idname = "view3d.dreamuv_uvcycle" 9 | bl_label = "3D View UV Cycle" 10 | bl_options = {"UNDO"} 11 | 12 | def execute(self, context): 13 | mesh = bpy.context.object.data 14 | bm = bmesh.from_edit_mesh(mesh) 15 | bm.faces.ensure_lookup_table() 16 | uv_layer = bm.loops.layers.uv.active 17 | 18 | #do this again 19 | faces = list() 20 | #MAKE FACE LIST 21 | for face in bm.faces: 22 | if face.select: 23 | faces.append(face) 24 | 25 | #get original size 26 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 27 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 28 | 29 | for face in faces: 30 | for vert in face.loops: 31 | xmin = min(xmin, vert[uv_layer].uv.x) 32 | xmax = max(xmax, vert[uv_layer].uv.x) 33 | ymin = min(ymin, vert[uv_layer].uv.y) 34 | ymax = max(ymax, vert[uv_layer].uv.y) 35 | 36 | #prevent divide by 0: 37 | if (xmax - xmin) == 0: 38 | xmin = .1 39 | if (ymax - ymin) == 0: 40 | ymin = .1 41 | 42 | for face in faces: 43 | for loop in face.loops: 44 | 45 | loop[uv_layer].uv.x -= xmin 46 | loop[uv_layer].uv.y -= ymin 47 | loop[uv_layer].uv.x /= (xmax-xmin) 48 | loop[uv_layer].uv.y /= (ymax-ymin) 49 | 50 | newx = -loop[uv_layer].uv.y + 1.0 51 | newy = loop[uv_layer].uv.x 52 | loop[uv_layer].uv.x = newx 53 | loop[uv_layer].uv.y = newy 54 | 55 | loop[uv_layer].uv.x *= xmax-xmin 56 | loop[uv_layer].uv.y *= ymax-ymin 57 | loop[uv_layer].uv.x += xmin 58 | loop[uv_layer].uv.y += ymin 59 | 60 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 61 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVExtend.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | from mathutils import Vector 5 | 6 | class DREAMUV_OT_uv_extend(bpy.types.Operator): 7 | """Extend UVs on selected faces from active face""" 8 | bl_idname = "view3d.dreamuv_uvextend" 9 | bl_label = "3D View UV Extend" 10 | bl_options = {"UNDO"} 11 | 12 | def execute(self, context): 13 | mesh = bpy.context.object.data 14 | bm = bmesh.from_edit_mesh(mesh) 15 | bm.faces.ensure_lookup_table() 16 | 17 | uv_layer = bm.loops.layers.uv.active #active uv layer! 18 | 19 | facecounter = 0 20 | 21 | face0=[] 22 | face1=[] 23 | 24 | #count to make sure only 2 faces selected 25 | for f in bm.faces: 26 | if f.select: 27 | facecounter += 1 28 | if facecounter < 2: 29 | self.report({'INFO'}, "only one face selected, aborting") 30 | return {'FINISHED'} 31 | 32 | 33 | #save active face! 34 | for l in bm.faces.active.loops: 35 | face1.append(l) 36 | 37 | #save other face 38 | for f in bm.faces: 39 | if f.select: 40 | if f is not bm.faces.active: 41 | for l in f.loops: 42 | face0.append(l) 43 | else: 44 | f.select=False 45 | 46 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 47 | 48 | 49 | #find first 2 shared vertices 50 | 51 | vert1 = None 52 | vert1uv0 = None 53 | vert1uv1 = None 54 | vert2 = None 55 | vert2uv0 = None 56 | vert2uv1 = None 57 | 58 | for l in face0: 59 | for l2 in face1: 60 | if l.vert.index == l2.vert.index and vert1 == None: 61 | vert1 = l.vert.index 62 | vert1uv0 = l[uv_layer].uv 63 | vert1uv1 = l2[uv_layer].uv 64 | for l in face0: 65 | for l2 in face1: 66 | if l.vert.index is not vert1: 67 | if l.vert.index == l2.vert.index and vert2 == None: 68 | vert2 = l.vert.index 69 | vert2uv0 = l[uv_layer].uv 70 | vert2uv1 = l2[uv_layer].uv 71 | 72 | if vert1 is None or vert2 is None: 73 | self.report({'INFO'}, "no shared edge found, aborting") 74 | return {'FINISHED'} 75 | 76 | #calculate angle 77 | TWOPI = 6.2831853071795865 78 | RAD2DEG = 57.2957795130823209 79 | a1,a2 = vert1uv0.x,vert1uv0.y 80 | b1,b2 = vert2uv0.x,vert2uv0.y 81 | theta = math.atan2(b1 - a1, a2 - b2)-(TWOPI/4) 82 | if theta < 0.0: 83 | theta += TWOPI 84 | angle1 = theta 85 | 86 | a1,a2 = vert1uv1.x,vert1uv1.y 87 | b1,b2 = vert2uv1.x,vert2uv1.y 88 | theta = math.atan2(b1 - a1, a2 - b2)-(TWOPI/4) 89 | if theta < 0.0: 90 | theta += TWOPI 91 | angle2 = theta 92 | 93 | angle=angle2-angle1 94 | if angle < 0.0: 95 | angle += TWOPI 96 | 97 | #move face0 to face1 98 | xdist = vert1uv0.x - vert1uv1.x 99 | ydist = vert1uv0.y - vert1uv1.y 100 | 101 | for l in face0: 102 | l[uv_layer].uv.x -= xdist 103 | l[uv_layer].uv.y -= ydist 104 | 105 | #rotate face0 106 | for l in face0: 107 | px = l[uv_layer].uv.x 108 | py = l[uv_layer].uv.y 109 | l[uv_layer].uv.x = math.cos(angle) * (px- vert1uv0.x) - math.sin(angle) * (py- vert1uv0.y) + vert1uv0.x 110 | l[uv_layer].uv.y = math.sin(angle) * (px - vert1uv0.x) + math.cos(angle) * (py- vert1uv0.y) + vert1uv0.y 111 | 112 | #find scale 113 | p1 = (vert2uv0.x+vert2uv0.y)-(vert1uv0.x+vert1uv0.y) 114 | p2 = (vert2uv1.x+vert2uv1.y)-(vert1uv0.x+vert1uv0.y) 115 | 116 | if p1 == 0.0: 117 | p1=0.01 118 | scaledelta = p2/p1 119 | 120 | 121 | ox=vert1uv0.x 122 | oy=vert1uv0.y 123 | 124 | #scale face0 125 | for l in face0: 126 | l[uv_layer].uv.x -= ox 127 | l[uv_layer].uv.y -= oy 128 | l[uv_layer].uv.x *= scaledelta 129 | l[uv_layer].uv.y *= scaledelta 130 | l[uv_layer].uv.x += ox 131 | l[uv_layer].uv.y += oy 132 | 133 | #bm.to_mesh(me) 134 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 135 | 136 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVInset.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | 5 | 6 | class DREAMUV_OT_uv_inset(bpy.types.Operator): 7 | """Inset UVs in the 3D Viewport""" 8 | bl_idname = "view3d.dreamuv_uvinset" 9 | bl_label = "UV Inset" 10 | bl_options = {"GRAB_CURSOR", "UNDO", "BLOCKING"} 11 | 12 | first_mouse_x = None 13 | first_value = None 14 | mesh = None 15 | bm = None 16 | bm2 = None 17 | 18 | xcenter=0 19 | ycenter=0 20 | 21 | shiftreset = False 22 | 23 | xlock=False 24 | ylock=False 25 | constrainttest = False 26 | 27 | s1=3 28 | s2=.5 29 | 30 | move_snap = 2 31 | 32 | def invoke(self, context, event): 33 | 34 | #object->edit switch seems to "lock" the data. Ugly but hey it works 35 | bpy.ops.object.mode_set(mode='OBJECT') 36 | bpy.ops.object.mode_set(mode='EDIT') 37 | 38 | self.shiftreset = False 39 | self.xlock=False 40 | self.ylock=False 41 | self.constrainttest = False 42 | 43 | self.scale_snap = 2 44 | module_name = __name__.split('.')[0] 45 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 46 | self.scale_snap = addon_prefs.scale_snap 47 | 48 | if context.object: 49 | self.first_mouse_x = event.mouse_x+1000/self.s1 50 | self.first_mouse_y = event.mouse_y+1000/self.s1 51 | 52 | self.mesh = bpy.context.object.data 53 | self.bm = bmesh.from_edit_mesh(self.mesh) 54 | 55 | #save original for reference 56 | self.bm2 = bmesh.new() 57 | self.bm2.from_mesh(self.mesh) 58 | 59 | #have to do this for some reason 60 | self.bm.faces.ensure_lookup_table() 61 | self.bm2.faces.ensure_lookup_table() 62 | 63 | #find "center" 64 | #loop through every selected face and move the uv's using original uv as reference 65 | xmin=0 66 | xmax=0 67 | ymin=0 68 | ymax=0 69 | first = True 70 | for i,face in enumerate(self.bm.faces): 71 | if face.select: 72 | for o,vert in enumerate(face.loops): 73 | if first: 74 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 75 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 76 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 77 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 78 | first=False 79 | else: 80 | if vert[self.bm.loops.layers.uv.active].uv.x < xmin: 81 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 82 | elif vert[self.bm.loops.layers.uv.active].uv.x > xmax: 83 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 84 | 85 | if vert[self.bm.loops.layers.uv.active].uv.y < ymin: 86 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 87 | elif vert[self.bm.loops.layers.uv.active].uv.y > ymax: 88 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 89 | 90 | self.xcenter=(xmin+xmax)/2 91 | self.ycenter=(ymin+ymax)/2 92 | 93 | context.window_manager.modal_handler_add(self) 94 | return {'RUNNING_MODAL'} 95 | else: 96 | self.report({'WARNING'}, "No active object") 97 | return {'CANCELLED'} 98 | 99 | def modal(self, context, event): 100 | 101 | if event.type == 'X': 102 | self.xlock=False 103 | self.ylock=True 104 | if event.type == 'Y': 105 | self.xlock=True 106 | self.ylock=False 107 | 108 | #test is middle mouse held down 109 | if event.type == 'MIDDLEMOUSE' and event.value == 'PRESS': 110 | self.constrainttest = True 111 | if event.type == 'MIDDLEMOUSE' and event.value == 'RELEASE': 112 | self.constrainttest = False 113 | 114 | #test if mouse is in the right quadrant for X or Y movement 115 | if self.constrainttest: 116 | mouseangle=math.atan2(event.mouse_y-self.first_mouse_y,event.mouse_x-self.first_mouse_x) 117 | mousetestx=False 118 | if (mouseangle < 0.785 and mouseangle > -0.785) or (mouseangle > 2.355 or mouseangle < -2.355): 119 | mousetestx=True 120 | if mousetestx: 121 | self.xlock=True 122 | self.ylock=False 123 | else: 124 | self.xlock=False 125 | self.ylock=True 126 | 127 | 128 | if event.type == 'MOUSEMOVE': 129 | 130 | deltax = self.first_mouse_x - event.mouse_x 131 | deltay = self.first_mouse_y - event.mouse_y 132 | 133 | 134 | if event.shift and not event.ctrl: 135 | #self.delta*=.1 136 | #reset origin position to shift into precision mode 137 | 138 | if not self.shiftreset: 139 | self.shiftreset=True 140 | self.first_mouse_x = event.mouse_x+1000/self.s2 141 | self.first_mouse_y = event.mouse_y+1000/self.s2 142 | for i,face in enumerate(self.bm.faces): 143 | if face.select: 144 | for o,vert in enumerate(face.loops): 145 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = vert[self.bm.loops.layers.uv.active].uv 146 | deltax = self.first_mouse_x - event.mouse_x 147 | deltay = self.first_mouse_y - event.mouse_y 148 | deltax*=0.001*self.s2 149 | deltay*=0.001*self.s2 150 | 151 | else: 152 | #reset origin position to shift into normal mode 153 | if self.shiftreset: 154 | self.shiftreset=False 155 | self.first_mouse_x = event.mouse_x+1000/self.s1 156 | self.first_mouse_y = event.mouse_y+1000/self.s1 157 | for i,face in enumerate(self.bm.faces): 158 | if face.select: 159 | for o,vert in enumerate(face.loops): 160 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = vert[self.bm.loops.layers.uv.active].uv 161 | deltax = self.first_mouse_x - event.mouse_x 162 | deltay = self.first_mouse_y - event.mouse_y 163 | deltax*=0.001*self.s1 164 | deltay*=0.001*self.s1 165 | 166 | if not self.xlock and not self.ylock: 167 | delta=(deltax+deltay)*.5 168 | deltax=delta 169 | deltay=delta 170 | 171 | if self.xlock: 172 | deltax=1 173 | 174 | if self.ylock: 175 | deltay=1 176 | 177 | if event.ctrl and not event.shift: 178 | deltax=math.floor(deltax*self.scale_snap)/self.scale_snap 179 | deltay=math.floor(deltay*self.scale_snap)/self.scale_snap 180 | if event.ctrl and event.shift: 181 | deltax=math.floor(deltax*self.scale_snap*self.scale_snap)/(self.scale_snap*self.scale_snap) 182 | deltay=math.floor(deltay*self.scale_snap*self.scale_snap)/(self.scale_snap*self.scale_snap) 183 | 184 | #loop through every selected face and move the uv's using original uv as reference 185 | for i,face in enumerate(self.bm.faces): 186 | if face.select: 187 | for o,vert in enumerate(face.loops): 188 | 189 | vert[self.bm.loops.layers.uv.active].uv.x=((deltax)*self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.x)+((1-(deltax))*self.xcenter) 190 | vert[self.bm.loops.layers.uv.active].uv.y=((deltay)*self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.y)+((1-(deltay))*self.ycenter) 191 | 192 | #update mesh 193 | bmesh.update_edit_mesh(self.mesh, False, False) 194 | 195 | elif event.type == 'LEFTMOUSE': 196 | 197 | #finish up and make sure changes are locked in place 198 | bpy.ops.object.mode_set(mode='OBJECT') 199 | bpy.ops.object.mode_set(mode='EDIT') 200 | return {'FINISHED'} 201 | 202 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 203 | 204 | #reset all uvs to reference 205 | for i,face in enumerate(self.bm.faces): 206 | if face.select: 207 | for o,vert in enumerate(face.loops): 208 | vert[self.bm.loops.layers.uv.active].uv = self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv 209 | #update mesh 210 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 211 | return {'CANCELLED'} 212 | 213 | return {'RUNNING_MODAL'} 214 | 215 | class DREAMUV_OT_uv_inset_step(bpy.types.Operator): 216 | """Inset UVs using pixel size""" 217 | bl_idname = "view3d.dreamuv_uvinsetstep" 218 | bl_label = "inset" 219 | bl_options = {"UNDO"} 220 | 221 | direction : bpy.props.StringProperty() 222 | 223 | def execute(self, context): 224 | mesh = bpy.context.object.data 225 | bm = bmesh.from_edit_mesh(mesh) 226 | bm.faces.ensure_lookup_table() 227 | uv_layer = bm.loops.layers.uv.active 228 | 229 | faces = list() 230 | #MAKE FACE LIST 231 | for face in bm.faces: 232 | if face.select: 233 | faces.append(face) 234 | 235 | #get original size 236 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 237 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 238 | 239 | for face in faces: 240 | for vert in face.loops: 241 | xmin = min(xmin, vert[uv_layer].uv.x) 242 | xmax = max(xmax, vert[uv_layer].uv.x) 243 | ymin = min(ymin, vert[uv_layer].uv.y) 244 | ymax = max(ymax, vert[uv_layer].uv.y) 245 | 246 | print(xmin) 247 | print(xmax) 248 | print(ymin) 249 | print(ymax) 250 | 251 | for face in faces: 252 | for loop in face.loops: 253 | loop[uv_layer].uv.x-= xmin 254 | loop[uv_layer].uv.y -= ymin 255 | loop[uv_layer].uv.x /= (xmax-xmin) 256 | loop[uv_layer].uv.y /= (ymax-ymin) 257 | 258 | pixel_inset = context.scene.uvinsetpixels/context.scene.uvinsettexsize 259 | 260 | if self.direction == "in": 261 | xmin += pixel_inset 262 | xmax -= pixel_inset 263 | ymin += pixel_inset 264 | ymax -= pixel_inset 265 | if self.direction == "out": 266 | xmin -= pixel_inset 267 | xmax += pixel_inset 268 | ymin -= pixel_inset 269 | ymax += pixel_inset 270 | 271 | #move into new quad 272 | for face in faces: 273 | for loop in face.loops: 274 | loop[uv_layer].uv.x *= xmax-xmin 275 | loop[uv_layer].uv.y *= ymax-ymin 276 | loop[uv_layer].uv.x += xmin 277 | loop[uv_layer].uv.y += ymin 278 | 279 | #update mesh 280 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 281 | 282 | 283 | 284 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVMirror.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import bmesh 4 | from mathutils import Vector 5 | 6 | class DREAMUV_OT_uv_mirror(bpy.types.Operator): 7 | """Mirror UVs but retain uv edge positions""" 8 | bl_idname = "view3d.dreamuv_uvmirror" 9 | bl_label = "3D View UV Mirror" 10 | bl_options = {"UNDO"} 11 | 12 | direction : bpy.props.StringProperty() 13 | 14 | def execute(self, context): 15 | mesh = bpy.context.object.data 16 | bm = bmesh.from_edit_mesh(mesh) 17 | bm.faces.ensure_lookup_table() 18 | uv_layer = bm.loops.layers.uv.active 19 | 20 | #do this again 21 | faces = list() 22 | #MAKE FACE LIST 23 | for face in bm.faces: 24 | if face.select: 25 | faces.append(face) 26 | 27 | #get original size 28 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 29 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 30 | 31 | for face in faces: 32 | for vert in face.loops: 33 | xmin = min(xmin, vert[uv_layer].uv.x) 34 | xmax = max(xmax, vert[uv_layer].uv.x) 35 | ymin = min(ymin, vert[uv_layer].uv.y) 36 | ymax = max(ymax, vert[uv_layer].uv.y) 37 | 38 | #prevent divide by 0: 39 | if (xmax - xmin) == 0: 40 | xmin = .1 41 | if (ymax - ymin) == 0: 42 | ymin = .1 43 | 44 | for face in faces: 45 | for loop in face.loops: 46 | loop[uv_layer].uv.x -= xmin 47 | loop[uv_layer].uv.y -= ymin 48 | loop[uv_layer].uv.x /= (xmax-xmin) 49 | loop[uv_layer].uv.y /= (ymax-ymin) 50 | 51 | if self.direction == "x": 52 | loop[uv_layer].uv.x = -loop[uv_layer].uv.x + 1.0 53 | if self.direction == "y": 54 | loop[uv_layer].uv.y = -loop[uv_layer].uv.y + 1.0 55 | 56 | loop[uv_layer].uv.x *= xmax-xmin 57 | loop[uv_layer].uv.y *= ymax-ymin 58 | loop[uv_layer].uv.x += xmin 59 | loop[uv_layer].uv.y += ymin 60 | 61 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 62 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVMoveToEdge.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | import bmesh 4 | from mathutils import Vector 5 | 6 | class DREAMUV_OT_uv_move_to_edge(bpy.types.Operator): 7 | """Move Selected faces to edge of texture""" 8 | bl_idname = "view3d.dreamuv_uvmovetoedge" 9 | bl_label = "3D View UV Move to UV Edge" 10 | bl_options = {"UNDO"} 11 | 12 | direction : bpy.props.StringProperty() 13 | 14 | def execute(self, context): 15 | 16 | mesh = bpy.context.object.data 17 | mesh = bpy.context.object.data 18 | bm = bmesh.from_edit_mesh(mesh) 19 | 20 | bpy.ops.uv.select_all(action='SELECT') 21 | 22 | xmin,xmax,ymin,ymax=0,0,0,0 23 | 24 | first = True 25 | for face in bm.faces: 26 | if face.select: 27 | for l in face.loops: 28 | if l[bm.loops.layers.uv.active].select: 29 | if first: 30 | xmin = l[bm.loops.layers.uv.active].uv.x 31 | xmax = l[bm.loops.layers.uv.active].uv.x 32 | ymin = l[bm.loops.layers.uv.active].uv.y 33 | ymax = l[bm.loops.layers.uv.active].uv.y 34 | first=False 35 | else: 36 | if l[bm.loops.layers.uv.active].uv.x < xmin: 37 | xmin = l[bm.loops.layers.uv.active].uv.x 38 | elif l[bm.loops.layers.uv.active].uv.x > xmax: 39 | xmax = l[bm.loops.layers.uv.active].uv.x 40 | if l[bm.loops.layers.uv.active].uv.y < ymin: 41 | ymin = l[bm.loops.layers.uv.active].uv.y 42 | elif l[bm.loops.layers.uv.active].uv.y > ymax: 43 | ymax = l[bm.loops.layers.uv.active].uv.y 44 | 45 | xdist = 0 46 | ydist = 0 47 | 48 | if self.direction == "up": 49 | ydist = 1-ymax 50 | if self.direction == "down": 51 | ydist = -ymin 52 | if self.direction == "right": 53 | xdist = 1-xmax 54 | if self.direction == "left": 55 | xdist = -xmin 56 | 57 | 58 | for face in bm.faces: 59 | if face.select: 60 | for l in face.loops: 61 | if l[bm.loops.layers.uv.active].select: 62 | l[bm.loops.layers.uv.active].uv.x += xdist 63 | l[bm.loops.layers.uv.active].uv.y += ydist 64 | 65 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 66 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVProject.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "HotSpotter", 3 | "category": "3D View", 4 | "author": "brame@valvesoftware.com", 5 | "description": "Adds source 2 Utilities to the scene properties tab", 6 | "blender": (2, 80, 0) 7 | } 8 | 9 | import bpy 10 | import bmesh 11 | import math 12 | from mathutils import Vector 13 | from . import DUV_Utils 14 | 15 | 16 | class DREAMUV_OT_uv_project(bpy.types.Operator): 17 | bl_idname = "view3d.dreamuv_uvproject" 18 | bl_label = "project along world axis!" 19 | 20 | def execute(self, context): 21 | obj = bpy.context.view_layer.objects.active 22 | bm = bmesh.from_edit_mesh(obj.data) 23 | uv_layer = bm.loops.layers.uv.verify() 24 | faces = list() 25 | 26 | #MAKE FACE LIST 27 | for face in bm.faces: 28 | if face.select: 29 | faces.append(face) 30 | 31 | for face in faces: 32 | for loop in face.loops: 33 | loop_uv = loop[uv_layer] 34 | # use xy position of the vertex as a uv coordinate (OBJECT SPACE) 35 | worldcoords = obj.matrix_world @ loop.vert.co 36 | loop_uv.uv = worldcoords.xy 37 | 38 | #FIT TO 0-1 range 39 | print("fitting it") 40 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 41 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 42 | 43 | for face in faces: 44 | for vert in face.loops: 45 | xmin = min(xmin, vert[uv_layer].uv.x) 46 | xmax = max(xmax, vert[uv_layer].uv.x) 47 | ymin = min(ymin, vert[uv_layer].uv.y) 48 | ymax = max(ymax, vert[uv_layer].uv.y) 49 | 50 | for face in faces: 51 | for loop in face.loops: 52 | loop[uv_layer].uv.x -= xmin 53 | loop[uv_layer].uv.y -= ymin 54 | loop[uv_layer].uv.x /= (xmax-xmin) 55 | loop[uv_layer].uv.y /= (ymax-ymin) 56 | 57 | bmesh.update_edit_mesh(obj.data) 58 | 59 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVRotate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | 5 | 6 | class DREAMUV_OT_uv_rotate(bpy.types.Operator): 7 | """Rotate UVs in the 3D Viewport""" 8 | bl_idname = "view3d.dreamuv_uvrotate" 9 | bl_label = "UV Rotate" 10 | bl_options = {"GRAB_CURSOR", "UNDO", "BLOCKING"} 11 | 12 | first_mouse_x = None 13 | first_value = None 14 | mesh = None 15 | bm = None 16 | bm2 = None 17 | 18 | xcenter=0 19 | ycenter=0 20 | 21 | startdelta=0 22 | 23 | rotate_snap = 45 24 | 25 | def invoke(self, context, event): 26 | 27 | #object->edit switch seems to "lock" the data. Ugly but hey it works 28 | bpy.ops.object.mode_set(mode='OBJECT') 29 | bpy.ops.object.mode_set(mode='EDIT') 30 | 31 | self.rotate_snap = 45 32 | module_name = __name__.split('.')[0] 33 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 34 | self.rotate_snap = addon_prefs.rotate_snap 35 | 36 | if context.object: 37 | #self.first_mouse_x = event.mouse_x 38 | #self.first_mouse_y = event.mouse_y 39 | 40 | #test: set rotation center to viewport center for now 41 | #test: possibly change this to selection center? 42 | self.first_mouse_x = (bpy.context.region.width/2)+bpy.context.region.x 43 | self.first_mouse_y = (bpy.context.region.height/2)+bpy.context.region.y 44 | 45 | #get neutral angle from start location 46 | self.startdelta=math.atan2(event.mouse_y-self.first_mouse_y,event.mouse_x-self.first_mouse_x) 47 | 48 | self.mesh = bpy.context.object.data 49 | self.bm = bmesh.from_edit_mesh(self.mesh) 50 | 51 | #save original for reference 52 | self.bm2 = bmesh.new() 53 | self.bm2.from_mesh(self.mesh) 54 | 55 | #have to do this for some reason 56 | self.bm.faces.ensure_lookup_table() 57 | self.bm2.faces.ensure_lookup_table() 58 | 59 | #find "center" 60 | xmin=0 61 | xmax=0 62 | ymin=0 63 | ymax=0 64 | first = True 65 | for i,face in enumerate(self.bm.faces): 66 | if face.select: 67 | for o,vert in enumerate(face.loops): 68 | if first: 69 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 70 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 71 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 72 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 73 | first=False 74 | else: 75 | if vert[self.bm.loops.layers.uv.active].uv.x < xmin: 76 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 77 | elif vert[self.bm.loops.layers.uv.active].uv.x > xmax: 78 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 79 | 80 | if vert[self.bm.loops.layers.uv.active].uv.y < ymin: 81 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 82 | elif vert[self.bm.loops.layers.uv.active].uv.y > ymax: 83 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 84 | 85 | self.xcenter=(xmin+xmax)/2 86 | self.ycenter=(ymin+ymax)/2 87 | 88 | context.window_manager.modal_handler_add(self) 89 | return {'RUNNING_MODAL'} 90 | else: 91 | self.report({'WARNING'}, "No active object") 92 | return {'CANCELLED'} 93 | 94 | def modal(self, context, event): 95 | 96 | if event.type == 'MOUSEMOVE': 97 | 98 | #get angle of cursor from start pos in radians 99 | delta = -math.atan2(event.mouse_y-self.first_mouse_y,event.mouse_x-self.first_mouse_x) 100 | #neutralize angle for mouse start position 101 | delta+=self.startdelta 102 | 103 | 104 | vcenterx = (bpy.context.region.width/2)+bpy.context.region.x 105 | 106 | #step rotation 107 | if event.ctrl and not event.shift: 108 | 109 | #PI/4=0.78539816339 110 | PIdiv=3.14159265359/(180/self.rotate_snap) 111 | delta=math.floor(delta/PIdiv)*PIdiv 112 | if event.ctrl and event.shift: 113 | PIdiv=3.14159265359/(180/self.rotate_snap)/2 114 | delta=math.floor(delta/PIdiv)*PIdiv 115 | 116 | #loop through every selected face and scale the uv's using original uv as reference 117 | for i,face in enumerate(self.bm.faces): 118 | if face.select: 119 | for o,vert in enumerate(face.loops): 120 | 121 | px=self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.x 122 | py=self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.y 123 | 124 | vert[self.bm.loops.layers.uv.active].uv.x = math.cos(delta) * (px-self.xcenter) - math.sin(delta) * (py-self.ycenter) + self.xcenter 125 | vert[self.bm.loops.layers.uv.active].uv.y = math.sin(delta) * (px-self.xcenter) + math.cos(delta) * (py-self.ycenter) + self.ycenter 126 | 127 | #update mesh 128 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 129 | 130 | elif event.type == 'LEFTMOUSE': 131 | 132 | #finish up and make sure changes are locked in place 133 | bpy.ops.object.mode_set(mode='OBJECT') 134 | bpy.ops.object.mode_set(mode='EDIT') 135 | return {'FINISHED'} 136 | 137 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 138 | 139 | # reset all uvs to reference 140 | delta=0 141 | for i,face in enumerate(self.bm.faces): 142 | if face.select: 143 | for o,vert in enumerate(face.loops): 144 | 145 | px=self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.x 146 | py=self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.y 147 | 148 | vert[self.bm.loops.layers.uv.active].uv.x = math.cos(delta) * (px-self.xcenter) - math.sin(delta) * (py-self.ycenter) + self.xcenter 149 | vert[self.bm.loops.layers.uv.active].uv.y = math.sin(delta) * (px-self.xcenter) + math.cos(delta) * (py-self.ycenter) + self.ycenter 150 | 151 | #update mesh 152 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 153 | return {'CANCELLED'} 154 | 155 | return {'RUNNING_MODAL'} 156 | 157 | class DREAMUV_OT_uv_rotate_step(bpy.types.Operator): 158 | """Rotate UVs using snap size""" 159 | bl_idname = "view3d.dreamuv_uvrotatestep" 160 | bl_label = "rotate" 161 | bl_options = {"UNDO"} 162 | 163 | direction : bpy.props.StringProperty() 164 | 165 | def execute(self, context): 166 | mesh = bpy.context.object.data 167 | bm = bmesh.from_edit_mesh(mesh) 168 | bm.faces.ensure_lookup_table() 169 | uv_layer = bm.loops.layers.uv.active 170 | 171 | faces = list() 172 | #MAKE FACE LIST 173 | for face in bm.faces: 174 | if face.select: 175 | faces.append(face) 176 | 177 | mirrored = False 178 | #check if mirrored: 179 | for face in faces: 180 | sum_edges = 0 181 | # Only loop 3 verts ignore others: faster! 182 | for i in range(3): 183 | uv_A = face.loops[i][uv_layer].uv 184 | uv_B = face.loops[(i+1)%3][uv_layer].uv 185 | sum_edges += (uv_B.x - uv_A.x) * (uv_B.y + uv_A.y) 186 | 187 | if sum_edges > 0: 188 | mirrored = True 189 | 190 | #get original size 191 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 192 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 193 | 194 | for face in faces: 195 | for vert in face.loops: 196 | xmin = min(xmin, vert[uv_layer].uv.x) 197 | xmax = max(xmax, vert[uv_layer].uv.x) 198 | ymin = min(ymin, vert[uv_layer].uv.y) 199 | ymax = max(ymax, vert[uv_layer].uv.y) 200 | 201 | xcenter=(xmin+xmax)/2 202 | ycenter=(ymin+ymax)/2 203 | 204 | #step rotation 205 | module_name = __name__.split('.')[0] 206 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 207 | rotate_snap = addon_prefs.rotate_snap 208 | print(rotate_snap) 209 | 210 | #PI/4=0.78539816339 211 | PIdiv=3.14159265359/(180/rotate_snap) 212 | delta = (3.14159265359/180)*rotate_snap 213 | #delta = math.floor(delta/PIdiv)*PIdiv 214 | if self.direction == "reverse": 215 | print("reverse") 216 | #delta = (3.14159265359/180)-delta 217 | delta = -delta 218 | if mirrored: 219 | delta = -delta 220 | 221 | #loop through every selected face and scale the uv's using original uv as reference 222 | for face in faces: 223 | for loop in face.loops: 224 | loop[uv_layer].uv.x -= xcenter 225 | loop[uv_layer].uv.y -= ycenter 226 | 227 | oldx = loop[uv_layer].uv.x 228 | oldy = loop[uv_layer].uv.y 229 | 230 | loop[uv_layer].uv.x = oldx * math.cos(delta) - oldy * math.sin(delta) 231 | loop[uv_layer].uv.y = oldy * math.cos(delta) + oldx * math.sin(delta) 232 | 233 | loop[uv_layer].uv.x += xcenter 234 | loop[uv_layer].uv.y += ycenter 235 | 236 | 237 | #update mesh 238 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 239 | 240 | 241 | 242 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVScale.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | 5 | 6 | class DREAMUV_OT_uv_scale(bpy.types.Operator): 7 | """Scale UVs in the 3D Viewport""" 8 | bl_idname = "view3d.dreamuv_uvscale" 9 | bl_label = "UV Scale" 10 | bl_options = {"GRAB_CURSOR", "UNDO", "BLOCKING"} 11 | 12 | first_mouse_x = None 13 | first_value = None 14 | mesh = None 15 | bm = None 16 | bm2 = None 17 | 18 | xcenter=0 19 | ycenter=0 20 | 21 | shiftreset = False 22 | 23 | xlock=False 24 | ylock=False 25 | constrainttest = False 26 | 27 | s1=3 28 | s2=.5 29 | 30 | move_snap = 2 31 | 32 | def invoke(self, context, event): 33 | 34 | #object->edit switch seems to "lock" the data. Ugly but hey it works 35 | bpy.ops.object.mode_set(mode='OBJECT') 36 | bpy.ops.object.mode_set(mode='EDIT') 37 | 38 | self.shiftreset = False 39 | self.xlock=False 40 | self.ylock=False 41 | self.constrainttest = False 42 | 43 | self.scale_snap = 2 44 | module_name = __name__.split('.')[0] 45 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 46 | self.scale_snap = addon_prefs.scale_snap 47 | 48 | if context.object: 49 | self.first_mouse_x = event.mouse_x+1000/self.s1 50 | self.first_mouse_y = event.mouse_y+1000/self.s1 51 | 52 | self.mesh = bpy.context.object.data 53 | self.bm = bmesh.from_edit_mesh(self.mesh) 54 | 55 | #save original for reference 56 | self.bm2 = bmesh.new() 57 | self.bm2.from_mesh(self.mesh) 58 | 59 | #have to do this for some reason 60 | self.bm.faces.ensure_lookup_table() 61 | self.bm2.faces.ensure_lookup_table() 62 | 63 | #find "center" 64 | #loop through every selected face and move the uv's using original uv as reference 65 | xmin=0 66 | xmax=0 67 | ymin=0 68 | ymax=0 69 | first = True 70 | for i,face in enumerate(self.bm.faces): 71 | if face.select: 72 | for o,vert in enumerate(face.loops): 73 | if first: 74 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 75 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 76 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 77 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 78 | first=False 79 | else: 80 | if vert[self.bm.loops.layers.uv.active].uv.x < xmin: 81 | xmin=vert[self.bm.loops.layers.uv.active].uv.x 82 | elif vert[self.bm.loops.layers.uv.active].uv.x > xmax: 83 | xmax=vert[self.bm.loops.layers.uv.active].uv.x 84 | 85 | if vert[self.bm.loops.layers.uv.active].uv.y < ymin: 86 | ymin=vert[self.bm.loops.layers.uv.active].uv.y 87 | elif vert[self.bm.loops.layers.uv.active].uv.y > ymax: 88 | ymax=vert[self.bm.loops.layers.uv.active].uv.y 89 | 90 | self.xcenter=(xmin+xmax)/2 91 | self.ycenter=(ymin+ymax)/2 92 | 93 | context.window_manager.modal_handler_add(self) 94 | return {'RUNNING_MODAL'} 95 | else: 96 | self.report({'WARNING'}, "No active object") 97 | return {'CANCELLED'} 98 | 99 | def modal(self, context, event): 100 | 101 | if event.type == 'X': 102 | self.xlock=False 103 | self.ylock=True 104 | if event.type == 'Y': 105 | self.xlock=True 106 | self.ylock=False 107 | 108 | #test is middle mouse held down 109 | if event.type == 'MIDDLEMOUSE' and event.value == 'PRESS': 110 | self.constrainttest = True 111 | if event.type == 'MIDDLEMOUSE' and event.value == 'RELEASE': 112 | self.constrainttest = False 113 | 114 | #test if mouse is in the right quadrant for X or Y movement 115 | if self.constrainttest: 116 | mouseangle=math.atan2(event.mouse_y-self.first_mouse_y,event.mouse_x-self.first_mouse_x) 117 | mousetestx=False 118 | if (mouseangle < 0.785 and mouseangle > -0.785) or (mouseangle > 2.355 or mouseangle < -2.355): 119 | mousetestx=True 120 | if mousetestx: 121 | self.xlock=True 122 | self.ylock=False 123 | else: 124 | self.xlock=False 125 | self.ylock=True 126 | 127 | 128 | if event.type == 'MOUSEMOVE': 129 | 130 | deltax = self.first_mouse_x - event.mouse_x 131 | deltay = self.first_mouse_y - event.mouse_y 132 | 133 | 134 | if event.shift and not event.ctrl: 135 | #self.delta*=.1 136 | #reset origin position to shift into precision mode 137 | 138 | if not self.shiftreset: 139 | self.shiftreset=True 140 | self.first_mouse_x = event.mouse_x+1000/self.s2 141 | self.first_mouse_y = event.mouse_y+1000/self.s2 142 | for i,face in enumerate(self.bm.faces): 143 | if face.select: 144 | for o,vert in enumerate(face.loops): 145 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = vert[self.bm.loops.layers.uv.active].uv 146 | deltax = self.first_mouse_x - event.mouse_x 147 | deltay = self.first_mouse_y - event.mouse_y 148 | deltax*=0.001*self.s2 149 | deltay*=0.001*self.s2 150 | 151 | else: 152 | #reset origin position to shift into normal mode 153 | if self.shiftreset: 154 | self.shiftreset=False 155 | self.first_mouse_x = event.mouse_x+1000/self.s1 156 | self.first_mouse_y = event.mouse_y+1000/self.s1 157 | for i,face in enumerate(self.bm.faces): 158 | if face.select: 159 | for o,vert in enumerate(face.loops): 160 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = vert[self.bm.loops.layers.uv.active].uv 161 | deltax = self.first_mouse_x - event.mouse_x 162 | deltay = self.first_mouse_y - event.mouse_y 163 | deltax*=0.001*self.s1 164 | deltay*=0.001*self.s1 165 | 166 | if not self.xlock and not self.ylock: 167 | delta=(deltax+deltay)*.5 168 | deltax=delta 169 | deltay=delta 170 | 171 | if self.xlock: 172 | deltax=1 173 | 174 | if self.ylock: 175 | deltay=1 176 | 177 | if event.ctrl and not event.shift: 178 | deltax=math.floor(deltax*self.scale_snap)/self.scale_snap 179 | deltay=math.floor(deltay*self.scale_snap)/self.scale_snap 180 | if event.ctrl and event.shift: 181 | deltax=math.floor(deltax*self.scale_snap*self.scale_snap)/(self.scale_snap*self.scale_snap) 182 | deltay=math.floor(deltay*self.scale_snap*self.scale_snap)/(self.scale_snap*self.scale_snap) 183 | 184 | #loop through every selected face and move the uv's using original uv as reference 185 | for i,face in enumerate(self.bm.faces): 186 | if face.select: 187 | for o,vert in enumerate(face.loops): 188 | 189 | vert[self.bm.loops.layers.uv.active].uv.x=((deltax)*self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.x)+((1-(deltax))*self.xcenter) 190 | vert[self.bm.loops.layers.uv.active].uv.y=((deltay)*self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv.y)+((1-(deltay))*self.ycenter) 191 | 192 | #update mesh 193 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 194 | 195 | elif event.type == 'LEFTMOUSE': 196 | 197 | #finish up and make sure changes are locked in place 198 | bpy.ops.object.mode_set(mode='OBJECT') 199 | bpy.ops.object.mode_set(mode='EDIT') 200 | return {'FINISHED'} 201 | 202 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 203 | 204 | #reset all uvs to reference 205 | for i,face in enumerate(self.bm.faces): 206 | if face.select: 207 | for o,vert in enumerate(face.loops): 208 | vert[self.bm.loops.layers.uv.active].uv = self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv 209 | #update mesh 210 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 211 | return {'CANCELLED'} 212 | 213 | return {'RUNNING_MODAL'} 214 | 215 | class DREAMUV_OT_uv_scale_step(bpy.types.Operator): 216 | """Scale UVs using snap size""" 217 | bl_idname = "view3d.dreamuv_uvscalestep" 218 | bl_label = "scale" 219 | bl_options = {"UNDO"} 220 | 221 | direction : bpy.props.StringProperty() 222 | 223 | def execute(self, context): 224 | mesh = bpy.context.object.data 225 | bm = bmesh.from_edit_mesh(mesh) 226 | bm.faces.ensure_lookup_table() 227 | uv_layer = bm.loops.layers.uv.active 228 | 229 | faces = list() 230 | #MAKE FACE LIST 231 | for face in bm.faces: 232 | if face.select: 233 | faces.append(face) 234 | 235 | mirrored = False 236 | #check if mirrored: 237 | for face in faces: 238 | sum_edges = 0 239 | # Only loop 3 verts ignore others: faster! 240 | for i in range(3): 241 | uv_A = face.loops[i][uv_layer].uv 242 | uv_B = face.loops[(i+1)%3][uv_layer].uv 243 | sum_edges += (uv_B.x - uv_A.x) * (uv_B.y + uv_A.y) 244 | 245 | if sum_edges > 0: 246 | mirrored = True 247 | 248 | #get original size 249 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 250 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 251 | 252 | for face in faces: 253 | for vert in face.loops: 254 | xmin = min(xmin, vert[uv_layer].uv.x) 255 | xmax = max(xmax, vert[uv_layer].uv.x) 256 | ymin = min(ymin, vert[uv_layer].uv.y) 257 | ymax = max(ymax, vert[uv_layer].uv.y) 258 | 259 | xcenter=(xmin+xmax)/2 260 | ycenter=(ymin+ymax)/2 261 | 262 | #step rotation 263 | module_name = __name__.split('.')[0] 264 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 265 | scale_snap_x = addon_prefs.scale_snap 266 | scale_snap_y = addon_prefs.scale_snap 267 | 268 | if self.direction == "+XY": 269 | scale_snap_x = 1/scale_snap_x 270 | scale_snap_y = 1/scale_snap_y 271 | #if self.direction == "-XY": 272 | 273 | if self.direction == "+X": 274 | scale_snap_x = 1/scale_snap_x 275 | scale_snap_y = 1 276 | if self.direction == "-X": 277 | scale_snap_x = scale_snap_x 278 | scale_snap_y = 1 279 | if self.direction == "+Y": 280 | scale_snap_x = 1 281 | scale_snap_y = 1/scale_snap_y 282 | if self.direction == "-Y": 283 | scale_snap_x = 1 284 | scale_snap_y = scale_snap_y 285 | 286 | #PI/4=0.78539816339 287 | #PIdiv=3.14159265359/(180/rotate_snap) 288 | #delta = (3.14159265359/180)*rotate_snap 289 | #delta = math.floor(delta/PIdiv)*PIdiv 290 | #if self.direction == "reverse": 291 | # print("reverse") 292 | #delta = (3.14159265359/180)-delta 293 | # delta = -delta 294 | #if mirrored: 295 | # delta = -delta 296 | 297 | #loop through every selected face and scale the uv's using original uv as reference 298 | for face in faces: 299 | for loop in face.loops: 300 | loop[uv_layer].uv.x -= xcenter 301 | loop[uv_layer].uv.y -= ycenter 302 | 303 | #oldx = loop[uv_layer].uv.x 304 | #oldy = loop[uv_layer].uv.y 305 | 306 | loop[uv_layer].uv.x = loop[uv_layer].uv.x * scale_snap_x 307 | loop[uv_layer].uv.y = loop[uv_layer].uv.y * scale_snap_y 308 | 309 | loop[uv_layer].uv.x += xcenter 310 | loop[uv_layer].uv.y += ycenter 311 | 312 | 313 | #update mesh 314 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 315 | 316 | 317 | 318 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVStitch.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | class DREAMUV_OT_uv_stitch(bpy.types.Operator): 4 | """Stitch shared vertices on selected faces""" 5 | bl_idname = "view3d.dreamuv_uvstitch" 6 | bl_label = "3D View UV Stitch" 7 | bl_options = {"UNDO"} 8 | 9 | def execute(self, context): 10 | #yup, it's this simple 11 | bpy.ops.uv.select_all(action='SELECT') 12 | #2 stitch operations, possibly need more 13 | bpy.ops.uv.stitch(use_limit=False,snap_islands=False,midpoint_snap=True) 14 | bpy.ops.uv.stitch(use_limit=False,snap_islands=False,midpoint_snap=True) 15 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVTransfer.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | from bpy.types import Menu 4 | from bpy.props import EnumProperty, BoolProperty 5 | 6 | class DREAMUV_OT_uv_transfer(bpy.types.Operator): 7 | """Transfer to selection""" 8 | bl_idname = "view3d.dreamuv_uvtransfer" 9 | bl_label = "UV Transfer" 10 | bl_options = {"UNDO"} 11 | 12 | def execute(self, context): 13 | 14 | bpy.ops.uv.select_split() 15 | 16 | obj = bpy.context.view_layer.objects.active 17 | bm = bmesh.from_edit_mesh(obj.data) 18 | uv_layer = bm.loops.layers.uv.verify() 19 | 20 | faces = list() 21 | 22 | xmin,xmax,ymin,ymax=0,0,0,0 23 | 24 | selected_uv_loops = list() 25 | 26 | for i,face in enumerate(bm.faces): 27 | if face.select: 28 | for o,vert in enumerate(face.loops): 29 | if vert[bm.loops.layers.uv.active].select: 30 | selected_uv_loops.append(vert) 31 | 32 | #if nothing is selected, match UV selection to mesh selection 33 | 34 | if len(selected_uv_loops) == 0: 35 | for i,face in enumerate(bm.faces): 36 | if face.select: 37 | selected_uv_loops.extend(face.loops) 38 | 39 | first = True 40 | for vert in selected_uv_loops: 41 | if first: 42 | xmin=vert[bm.loops.layers.uv.active].uv.x 43 | xmax=vert[bm.loops.layers.uv.active].uv.x 44 | ymin=vert[bm.loops.layers.uv.active].uv.y 45 | ymax=vert[bm.loops.layers.uv.active].uv.y 46 | first=False 47 | else: 48 | if vert[bm.loops.layers.uv.active].uv.x < xmin: 49 | xmin=vert[bm.loops.layers.uv.active].uv.x 50 | elif vert[bm.loops.layers.uv.active].uv.x > xmax: 51 | xmax=vert[bm.loops.layers.uv.active].uv.x 52 | if vert[bm.loops.layers.uv.active].uv.y < ymin: 53 | ymin=vert[bm.loops.layers.uv.active].uv.y 54 | elif vert[bm.loops.layers.uv.active].uv.y > ymax: 55 | ymax=vert[bm.loops.layers.uv.active].uv.y 56 | 57 | 58 | aspect = (xmax-xmin)/(ymax-ymin) 59 | aspecttarget = (context.scene.uvtransferxmax-context.scene.uvtransferxmin)/(context.scene.uvtransferymax-context.scene.uvtransferymin) 60 | print("aspects") 61 | print(aspect) 62 | print(aspecttarget) 63 | 64 | 65 | #move to 0,1 66 | 67 | for vert in selected_uv_loops: 68 | vert[bm.loops.layers.uv.active].uv.x -= xmin 69 | vert[bm.loops.layers.uv.active].uv.y -= ymin 70 | vert[bm.loops.layers.uv.active].uv.x /= (xmax-xmin) 71 | vert[bm.loops.layers.uv.active].uv.y /= (ymax-ymin) 72 | 73 | xmin2 = .5 74 | ymin2 = .5 75 | 76 | xmax2 = 1 77 | ymax2 = 1 78 | 79 | #move to new rect 80 | 81 | for vert in selected_uv_loops: 82 | vert[bm.loops.layers.uv.active].uv.x = (vert[bm.loops.layers.uv.active].uv.x * (context.scene.uvtransferxmax-context.scene.uvtransferxmin)) + context.scene.uvtransferxmin 83 | vert[bm.loops.layers.uv.active].uv.y = (vert[bm.loops.layers.uv.active].uv.y * (context.scene.uvtransferymax-context.scene.uvtransferymin)) + context.scene.uvtransferymin 84 | 85 | 86 | bmesh.update_edit_mesh(obj.data) 87 | 88 | #cycle if needed: 89 | if (aspect >= 1 and aspecttarget < 1) or (aspect <= 1 and aspecttarget > 1): 90 | bpy.ops.view3d.dreamuv_uvcycle() 91 | return {'FINISHED'} 92 | 93 | class DREAMUV_OT_uv_transfer_grab(bpy.types.Operator): 94 | """UV Transfer Grab""" 95 | bl_idname = "view3d.dreamuv_uvtransfergrab" 96 | bl_label = "UV Transfer Grab" 97 | bl_options = {"UNDO"} 98 | 99 | def execute(self, context): 100 | bpy.ops.uv.select_split() 101 | 102 | obj = bpy.context.view_layer.objects.active 103 | bm = bmesh.from_edit_mesh(obj.data) 104 | uv_layer = bm.loops.layers.uv.verify() 105 | 106 | faces = list() 107 | 108 | xmin,xmax,ymin,ymax=0,0,0,0 109 | 110 | 111 | 112 | selected_uv_loops = list() 113 | 114 | for i,face in enumerate(bm.faces): 115 | if face.select: 116 | for o,vert in enumerate(face.loops): 117 | if vert[bm.loops.layers.uv.active].select: 118 | selected_uv_loops.append(vert) 119 | 120 | #if nothing is selected, match UV selection to mesh selection 121 | 122 | if len(selected_uv_loops) == 0: 123 | for i,face in enumerate(bm.faces): 124 | if face.select: 125 | selected_uv_loops.extend(face.loops) 126 | 127 | first = True 128 | for vert in selected_uv_loops: 129 | if first: 130 | xmin=vert[bm.loops.layers.uv.active].uv.x 131 | xmax=vert[bm.loops.layers.uv.active].uv.x 132 | ymin=vert[bm.loops.layers.uv.active].uv.y 133 | ymax=vert[bm.loops.layers.uv.active].uv.y 134 | first=False 135 | else: 136 | if vert[bm.loops.layers.uv.active].uv.x < xmin: 137 | xmin=vert[bm.loops.layers.uv.active].uv.x 138 | elif vert[bm.loops.layers.uv.active].uv.x > xmax: 139 | xmax=vert[bm.loops.layers.uv.active].uv.x 140 | if vert[bm.loops.layers.uv.active].uv.y < ymin: 141 | ymin=vert[bm.loops.layers.uv.active].uv.y 142 | elif vert[bm.loops.layers.uv.active].uv.y > ymax: 143 | ymax=vert[bm.loops.layers.uv.active].uv.y 144 | 145 | 146 | context.scene.uvtransferxmax = xmax 147 | context.scene.uvtransferxmin = xmin 148 | context.scene.uvtransferymax = ymax 149 | context.scene.uvtransferymin = ymin 150 | 151 | return {'FINISHED'} 152 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /DUV_UVTranslate.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | from mathutils import Vector 5 | from . import DUV_Utils 6 | 7 | 8 | class DREAMUV_OT_uv_translate(bpy.types.Operator): 9 | """Translate UVs in the 3D Viewport""" 10 | bl_idname = "view3d.dreamuv_uvtranslate" 11 | bl_label = "UV Translate" 12 | bl_options = {"GRAB_CURSOR", "UNDO", "BLOCKING"} 13 | 14 | first_mouse_x = None 15 | first_mouse_y = None 16 | first_value = None 17 | mesh = None 18 | bm = None 19 | bm2 = None 20 | bm_orig = None 21 | 22 | shiftreset = False 23 | delta = 0 24 | 25 | xlock = False 26 | ylock = False 27 | 28 | stateswitch = False 29 | mousetestx = False 30 | constrainttest = False 31 | 32 | pixel_steps = None 33 | do_pixel_snap = False 34 | 35 | move_snap = 4 36 | 37 | def invoke(self, context, event): 38 | 39 | self.shiftreset = False 40 | self.xlock = False 41 | self.ylock = False 42 | self.constrainttest = False 43 | self.stateswitch = False 44 | self.mousetestx = False 45 | 46 | self.pixel_steps = None 47 | self.do_pixel_snap = False 48 | 49 | self.move_snap = 0.25 50 | 51 | # object->edit switch seems to "lock" the data. Ugly but hey it works 52 | bpy.ops.object.mode_set(mode='OBJECT') 53 | bpy.ops.object.mode_set(mode='EDIT') 54 | 55 | if context.object: 56 | self.first_mouse_x = event.mouse_x 57 | self.first_mouse_y = event.mouse_y 58 | 59 | self.mesh = bpy.context.object.data 60 | self.bm = bmesh.from_edit_mesh(self.mesh) 61 | 62 | # save original for reference 63 | self.bm2 = bmesh.new() 64 | self.bm2.from_mesh(self.mesh) 65 | self.bm_orig = bmesh.new() 66 | self.bm_orig.from_mesh(self.mesh) 67 | 68 | # have to do this for some reason 69 | self.bm.faces.ensure_lookup_table() 70 | self.bm2.faces.ensure_lookup_table() 71 | self.bm_orig.faces.ensure_lookup_table() 72 | 73 | # Get refrerence to addon preference to get snap and scale setting 74 | module_name = __name__.split('.')[0] 75 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 76 | self.do_pixel_snap = addon_prefs.pixel_snap 77 | self.move_snap = addon_prefs.move_snap 78 | self.move_snap = 1/self.move_snap 79 | 80 | print(self.move_snap) 81 | # Precalculate data before going into modal 82 | self.pixel_steps = {} 83 | for i, face in enumerate(self.bm.faces): 84 | if face.select is False: 85 | continue 86 | # Find pixel steps per face here to look up in future translations 87 | if self.do_pixel_snap: 88 | pixel_step = DUV_Utils.get_face_pixel_step(context, face) 89 | if pixel_step is not None: 90 | self.pixel_steps[face.index] = pixel_step 91 | 92 | context.window_manager.modal_handler_add(self) 93 | return {'RUNNING_MODAL'} 94 | else: 95 | self.report({'WARNING'}, "No active object") 96 | return {'CANCELLED'} 97 | 98 | def modal(self, context, event): 99 | #context.area.header_text_set( 100 | # "DUV UVTranslate: X/Y - contrain along X/Y Axis, MMB drag - alternative axis contrain method, SHIFT - precision mode, CTRL - stepped mode, CTRL + SHIFT - stepped with smaller increments") 101 | #context.area.tag_redraw() 102 | 103 | # setup constraints first 104 | if event.type == 'X': 105 | self.stateswitch = True 106 | self.xlock = False 107 | self.ylock = True 108 | if event.type == 'Y': 109 | self.stateswitch = True 110 | self.xlock = True 111 | self.ylock = False 112 | 113 | # test is middle mouse held down 114 | if event.type == 'MIDDLEMOUSE' and event.value == 'PRESS': 115 | self.constrainttest = True 116 | if event.type == 'MIDDLEMOUSE' and event.value == 'RELEASE': 117 | self.constrainttest = False 118 | 119 | # test if mouse is in the right quadrant for X or Y movement 120 | if self.constrainttest: 121 | mouseangle = math.atan2(event.mouse_y - self.first_mouse_y, event.mouse_x - self.first_mouse_x) 122 | mousetestx = False 123 | if (mouseangle < 0.785 and mouseangle > -0.785) or (mouseangle > 2.355 or mouseangle < -2.355): 124 | mousetestx = True 125 | if mousetestx: 126 | self.xlock = False 127 | self.ylock = True 128 | else: 129 | self.xlock = True 130 | self.ylock = False 131 | if mousetestx is not self.mousetestx: 132 | self.stateswitch = True 133 | self.mousetestx = not self.mousetestx 134 | 135 | if self.stateswitch: 136 | self.stateswitch = False 137 | # reset to start editing from start position 138 | for i, face in enumerate(self.bm.faces): 139 | if face.select: 140 | for o, vert in enumerate(face.loops): 141 | reset_uv = self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv 142 | vert[self.bm.loops.layers.uv.active].uv = reset_uv 143 | 144 | if event.type == 'MOUSEMOVE': 145 | self.delta = ( 146 | (self.first_mouse_x - event.mouse_x), 147 | (self.first_mouse_y - event.mouse_y) 148 | ) 149 | 150 | sensitivity = 0.001 if not self.do_pixel_snap else 0.1 151 | 152 | self.delta = Vector(self.delta) * sensitivity 153 | 154 | if self.do_pixel_snap: 155 | self.delta.x = int(round(self.delta.x)) 156 | self.delta.y = int(round(self.delta.y)) 157 | 158 | if event.shift and not event.ctrl: 159 | self.delta *= .1 160 | # reset origin position to shift into precision mode 161 | if not self.shiftreset: 162 | self.shiftreset = True 163 | self.first_mouse_x = event.mouse_x 164 | self.first_mouse_y = event.mouse_y 165 | for i, face in enumerate(self.bm.faces): 166 | if face.select: 167 | for o, vert in enumerate(face.loops): 168 | reset_uv = vert[self.bm.loops.layers.uv.active].uv 169 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = reset_uv 170 | self.delta = (0, 0) 171 | self.delta = Vector(self.delta) 172 | 173 | else: 174 | # reset origin position to shift into normal mode 175 | if self.shiftreset: 176 | self.shiftreset = False 177 | self.first_mouse_x = event.mouse_x 178 | self.first_mouse_y = event.mouse_y 179 | for i, face in enumerate(self.bm.faces): 180 | if face.select: 181 | for o, vert in enumerate(face.loops): 182 | reset_uv = vert[self.bm.loops.layers.uv.active].uv 183 | self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv = reset_uv 184 | self.delta = (0, 0) 185 | self.delta = Vector(self.delta) 186 | 187 | if event.ctrl and not event.shift: 188 | self.delta.x = math.floor(self.delta.x * self.move_snap) / self.move_snap 189 | self.delta.y = math.floor(self.delta.y * self.move_snap) / self.move_snap 190 | if event.ctrl and event.shift: 191 | self.delta.x = math.floor(self.delta.x * (self.move_snap*self.move_snap)) / (self.move_snap*self.move_snap) 192 | self.delta.y = math.floor(self.delta.y * (self.move_snap*self.move_snap)) / (self.move_snap*self.move_snap) 193 | 194 | # loop through every selected face and move the uv's using original uv as reference 195 | for i, face in enumerate(self.bm.faces): 196 | if face.select is False: 197 | continue 198 | 199 | local_delta = self.delta.copy() 200 | if self.do_pixel_snap and face.index in self.pixel_steps.keys(): 201 | pixel_step = self.pixel_steps[face.index] 202 | local_delta.x *= pixel_step.x 203 | local_delta.y *= pixel_step.y 204 | 205 | uv_x_axis = Vector((1.0, 0.0)) 206 | uv_y_axis = Vector((0.0, 1.0)) 207 | 208 | if self.xlock: 209 | uv_x_axis = Vector((0, 0)) 210 | if self.ylock: 211 | uv_y_axis = Vector((0, 0)) 212 | 213 | for o, vert in enumerate(face.loops): 214 | origin_uv = self.bm2.faces[i].loops[o][self.bm2.loops.layers.uv.active].uv 215 | uv_offset = local_delta.x * uv_x_axis + local_delta.y * uv_y_axis 216 | vert[self.bm.loops.layers.uv.active].uv = origin_uv + uv_offset 217 | 218 | # update mesh 219 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 220 | 221 | elif event.type == 'LEFTMOUSE': 222 | # finish up and make sure changes are locked in place 223 | bpy.ops.object.mode_set(mode='OBJECT') 224 | bpy.ops.object.mode_set(mode='EDIT') 225 | return {'FINISHED'} 226 | 227 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 228 | 229 | # reset all uvs to reference 230 | for i, face in enumerate(self.bm.faces): 231 | if face.select: 232 | for o, vert in enumerate(face.loops): 233 | reset_uv = self.bm_orig.faces[i].loops[o][self.bm_orig.loops.layers.uv.active].uv 234 | vert[self.bm.loops.layers.uv.active].uv = reset_uv 235 | # update mesh 236 | bmesh.update_edit_mesh(self.mesh, loop_triangles=False, destructive=False) 237 | return {'CANCELLED'} 238 | 239 | return {'RUNNING_MODAL'} 240 | 241 | class DREAMUV_OT_uv_translate_step(bpy.types.Operator): 242 | """Move UVs using snap size""" 243 | bl_idname = "view3d.dreamuv_uvtranslatestep" 244 | bl_label = "UV Translate Step" 245 | bl_options = {"UNDO"} 246 | 247 | direction : bpy.props.StringProperty() 248 | 249 | def execute(self, context): 250 | mesh = bpy.context.object.data 251 | bm = bmesh.from_edit_mesh(mesh) 252 | bm.faces.ensure_lookup_table() 253 | uv_layer = bm.loops.layers.uv.active 254 | 255 | module_name = __name__.split('.')[0] 256 | addon_prefs = bpy.context.preferences.addons[module_name].preferences 257 | move_snap = addon_prefs.move_snap 258 | 259 | xmove = 0 260 | ymove = 0 261 | 262 | if self.direction == "left": 263 | xmove = move_snap 264 | if self.direction == "right": 265 | xmove = -move_snap 266 | if self.direction == "up": 267 | ymove = -move_snap 268 | if self.direction == "down": 269 | ymove = move_snap 270 | 271 | for face in bm.faces: 272 | if face.select: 273 | for loop in face.loops: 274 | loop[uv_layer].uv.x += xmove 275 | loop[uv_layer].uv.y += ymove 276 | 277 | #update mesh 278 | bmesh.update_edit_mesh(mesh, loop_triangles=False, destructive=False) 279 | 280 | 281 | 282 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_UVTrim.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | import random 5 | from mathutils import Vector 6 | from . import DUV_Utils 7 | 8 | def getymin(trim): 9 | return trim.ymin 10 | def getxmin(trim): 11 | return trim.xmin 12 | 13 | def read_trim_atlas(context, trimtype): 14 | atlas = list() 15 | obj = context.scene.trim_atlas 16 | me = obj.data 17 | bm = bmesh.new() 18 | bm.from_mesh(me) 19 | uv_layer = bm.loops.layers.uv.verify() 20 | #lets read coords 21 | 22 | faces = list() 23 | 24 | print("gettin atlas") 25 | 26 | for face in bm.faces: 27 | xmin, xmax = face.loops[0][uv_layer].uv.x, face.loops[0][uv_layer].uv.x 28 | ymin, ymax = face.loops[0][uv_layer].uv.y, face.loops[0][uv_layer].uv.y 29 | 30 | for vert in face.loops: 31 | xmin = min(xmin, vert[uv_layer].uv.x) 32 | xmax = max(xmax, vert[uv_layer].uv.x) 33 | ymin = min(ymin, vert[uv_layer].uv.y) 34 | ymax = max(ymax, vert[uv_layer].uv.y) 35 | 36 | horizontal = False 37 | vertical = False 38 | if xmin <= 0.01 and xmax >= 0.99: 39 | horizontal = True 40 | if ymin <= 0.01 and ymax >= 0.99: 41 | vertical = True 42 | 43 | facerect = trim() 44 | facerect.xmin = xmin 45 | facerect.ymin = ymin 46 | facerect.xmax = xmax 47 | facerect.ymax = ymax 48 | 49 | #print(trimtype) 50 | 51 | if (horizontal or vertical) and trimtype == "trim": 52 | print("adding trim") 53 | atlas.append(facerect) 54 | 55 | if trimtype == "cap" and not horizontal and not vertical: 56 | print("adding cap") 57 | #print(facerect.xmin) 58 | #print(facerect.xmax) 59 | atlas.append(facerect) 60 | 61 | if horizontal: 62 | atlas.sort(key=getymin, reverse=True) 63 | if vertical: 64 | atlas.sort(key=getxmin, reverse=False) 65 | if trimtype == "cap": 66 | atlas.sort(key=getxmin, reverse=False) 67 | 68 | 69 | #print(atlas) 70 | 71 | return atlas 72 | 73 | def uv_trim(context): 74 | #get atlas first 75 | atlas = read_trim_atlas(context, "trim") 76 | print("atlas:") 77 | print(atlas) 78 | #check if horizontal or vertical, and make trimsheet 79 | 80 | if context.scene.trim_index > ( len(atlas) - 1.0 ): 81 | context.scene.trim_index = 0.0 82 | 83 | 84 | #MAKE DUPLICATE AND SPLIT EDGES 85 | #CREATE WORKING DUPLICATE! 86 | object_original = bpy.context.view_layer.objects.active 87 | bpy.ops.object.editmode_toggle() 88 | bpy.ops.object.duplicate() 89 | bpy.ops.object.editmode_toggle() 90 | bpy.context.view_layer.objects.active.name = "dreamuv_temp" 91 | object_temporary = bpy.context.view_layer.objects.active 92 | 93 | #PREPROCESS - save seams and hard edges 94 | obj = bpy.context.view_layer.objects.active 95 | bm = bmesh.from_edit_mesh(obj.data) 96 | 97 | faces = list() 98 | for face in bm.faces: 99 | if face.select: 100 | faces.append(face) 101 | 102 | bmesh.update_edit_mesh(obj.data) 103 | 104 | #mark seams on selection edge: 105 | bpy.ops.mesh.region_to_loop() 106 | bpy.ops.mesh.mark_seam(clear=False) 107 | 108 | bpy.ops.mesh.select_all(action='DESELECT') 109 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='EDGE') 110 | 111 | #don't do angle 112 | #angle = bpy.context.object.data.auto_smooth_angle 113 | #bpy.ops.mesh.edges_select_sharp(sharpness=angle) 114 | #bpy.ops.mesh.mark_seam(clear=False) 115 | #bpy.ops.mesh.select_all(action='DESELECT') 116 | 117 | 118 | bpy.ops.mesh.mark_seam(clear=False) 119 | bpy.ops.mesh.select_all(action='DESELECT') 120 | 121 | for edge in bm.edges: 122 | if edge.seam or edge.smooth == False: 123 | edge.select = True 124 | 125 | bpy.ops.mesh.edge_split(type='EDGE') 126 | bpy.ops.mesh.select_all(action='DESELECT') 127 | 128 | 129 | 130 | #select all faces to be hotspotted again: 131 | 132 | for face in faces: 133 | face.select = True 134 | 135 | 136 | trimsheet = list() 137 | #for face in atlas: 138 | 139 | 140 | obj = bpy.context.view_layer.objects.active 141 | bm = bmesh.from_edit_mesh(obj.data) 142 | uv_layer = bm.loops.layers.uv.verify() 143 | 144 | HSfaces = list() 145 | #MAKE FACE LIST 146 | for face in bm.faces: 147 | if face.select: 148 | HSfaces.append(face) 149 | 150 | 151 | is_rect = DUV_Utils.square_fit(context) 152 | 153 | 154 | xmin, xmax = HSfaces[0].loops[0][uv_layer].uv.x, HSfaces[0].loops[0][uv_layer].uv.x 155 | ymin, ymax = HSfaces[0].loops[0][uv_layer].uv.y, HSfaces[0].loops[0][uv_layer].uv.y 156 | 157 | for face in HSfaces: 158 | for vert in face.loops: 159 | xmin = min(xmin, vert[uv_layer].uv.x) 160 | xmax = max(xmax, vert[uv_layer].uv.x) 161 | ymin = min(ymin, vert[uv_layer].uv.y) 162 | ymax = max(ymax, vert[uv_layer].uv.y) 163 | 164 | #test if tall or wide: 165 | width = xmax - xmin 166 | height = ymax - ymin 167 | 168 | horizontal = True 169 | if width <= height: 170 | horizontal = False 171 | 172 | #V1: assume horizontal layout 173 | if atlas[0].xmin <= 0.01 and atlas[0].xmax >= 0.99: 174 | print("HORIZONTAL MODE") 175 | 176 | #flip it! 177 | if horizontal == False: 178 | #flip width and height values: 179 | width = ymax - ymin 180 | height = xmax - xmin 181 | #rotate 182 | for face in HSfaces: 183 | for loop in face.loops: 184 | tempx = loop[uv_layer].uv.x 185 | loop[uv_layer].uv.x = loop[uv_layer].uv.y 186 | loop[uv_layer].uv.y = tempx 187 | 188 | #map trim to trimsheet index 189 | trimindex = int(context.scene.trim_index) 190 | scale = (atlas[trimindex].ymax-atlas[trimindex].ymin)/height 191 | 192 | #scale the uvs to sheet and move: 193 | for face in HSfaces: 194 | for loop in face.loops: 195 | loop[uv_layer].uv.x *= scale 196 | loop[uv_layer].uv.y *= scale 197 | #move 198 | loop[uv_layer].uv.y += atlas[trimindex].ymin 199 | 200 | #map to bounds: 201 | #first, get current bounds: 202 | bmin = 0 203 | bmax = 0 204 | for face in HSfaces: 205 | for loop in face.loops: 206 | if loop[uv_layer].uv.x > bmax: 207 | bmax = loop[uv_layer].uv.x 208 | 209 | #shift randomly 210 | random_shift = random.random() 211 | if context.scene.duv_uvtrim_randomshift == True: 212 | for face in HSfaces: 213 | for loop in face.loops: 214 | loop[uv_layer].uv.x += random_shift 215 | 216 | #else now scale to bounds: 217 | elif context.scene.duv_uvtrim_bounds == True: 218 | for face in HSfaces: 219 | for loop in face.loops: 220 | loop[uv_layer].uv.x /= bmax 221 | loop[uv_layer].uv.x *= (context.scene.duv_uvtrim_max - context.scene.duv_uvtrim_min) 222 | loop[uv_layer].uv.x += context.scene.duv_uvtrim_min 223 | 224 | if atlas[0].ymin <= 0.01 and atlas[0].ymax >= 0.99: 225 | print("VERTICAL MODE") 226 | #flip it! 227 | if not horizontal: 228 | print("this one is wrong") 229 | width = ymax - ymin 230 | height = xmax - xmin 231 | 232 | if horizontal: 233 | 234 | print("vertical mode and horizontal") 235 | 236 | #rotate 237 | for face in HSfaces: 238 | for loop in face.loops: 239 | tempx = loop[uv_layer].uv.x 240 | loop[uv_layer].uv.x = loop[uv_layer].uv.y 241 | loop[uv_layer].uv.y = tempx 242 | 243 | #map trim to trimsheet index 244 | trimindex = int(context.scene.trim_index) 245 | scale = (atlas[trimindex].xmax-atlas[trimindex].xmin)/height 246 | 247 | #scale the uvs to sheet and move: 248 | 249 | for face in HSfaces: 250 | for loop in face.loops: 251 | loop[uv_layer].uv.x *= scale 252 | loop[uv_layer].uv.y *= scale 253 | #loop[uv_layer].uv.y /= (xmax-xmin) 254 | 255 | #move 256 | loop[uv_layer].uv.x += atlas[trimindex].xmin 257 | 258 | #map to bounds: 259 | #first, get current bounds: 260 | bmin = 0 261 | bmax = 0 262 | for face in HSfaces: 263 | for loop in face.loops: 264 | if loop[uv_layer].uv.y > bmax: 265 | bmax = loop[uv_layer].uv.y 266 | 267 | #shift randomly 268 | random_shift = random.random() 269 | if context.scene.duv_uvtrim_randomshift == True: 270 | for face in HSfaces: 271 | for loop in face.loops: 272 | loop[uv_layer].uv.y += random_shift 273 | 274 | #else now scale to bounds: 275 | elif context.scene.duv_uvtrim_bounds == True: 276 | for face in HSfaces: 277 | for loop in face.loops: 278 | loop[uv_layer].uv.y /= bmax 279 | loop[uv_layer].uv.y *= (context.scene.duv_uvtrim_max - context.scene.duv_uvtrim_min) 280 | loop[uv_layer].uv.y += context.scene.duv_uvtrim_min 281 | 282 | 283 | 284 | 285 | bmesh.update_edit_mesh(obj.data) 286 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 287 | 288 | #NOW RETURN OLD UVS 289 | #transfer UV maps back to original mesh 290 | 291 | obj = bpy.context.view_layer.objects.active 292 | bm = bmesh.from_edit_mesh(obj.data) 293 | uv_layer = bm.loops.layers.uv.verify() 294 | uv_backup = list(); 295 | #print("new UV:") 296 | for face in bm.faces: 297 | backupface = list() 298 | for vert in face.loops: 299 | backupuv = list() 300 | backupuv.append(vert[uv_layer].uv.x) 301 | backupuv.append(vert[uv_layer].uv.y) 302 | backupface.append(backupuv) 303 | #print(backupuv) 304 | uv_backup.append(backupface) 305 | 306 | #now apply to original mesh 307 | bpy.ops.object.editmode_toggle() 308 | object_temporary.select_set(False) 309 | object_original.select_set(True) 310 | bpy.ops.object.editmode_toggle() 311 | 312 | obj = object_original 313 | bm = bmesh.from_edit_mesh(obj.data) 314 | uv_layer = bm.loops.layers.uv.verify() 315 | #uv_backup = list(); 316 | #print("new UV:") 317 | for face, backupface in zip(bm.faces, uv_backup): 318 | for vert, backupuv in zip(face.loops, backupface): 319 | vert[uv_layer].uv.x = backupuv[0] 320 | vert[uv_layer].uv.y = backupuv[1] 321 | bmesh.update_edit_mesh(obj.data) 322 | 323 | bpy.ops.object.editmode_toggle() 324 | 325 | object_original.select_set(False) 326 | object_temporary.select_set(True) 327 | bpy.ops.object.delete(use_global=False) 328 | object_original.select_set(True) 329 | context.view_layer.objects.active=bpy.context.selected_objects[0] 330 | 331 | #toggle back to edit mode 332 | bpy.ops.object.editmode_toggle() 333 | 334 | class DREAMUV_OT_uv_trim(bpy.types.Operator): 335 | """Unwrap selection as tiling trim using the trim atlas as a guide""" 336 | bl_idname = "view3d.dreamuv_uvtrim" 337 | bl_label = "Trim" 338 | bl_options = {"UNDO"} 339 | 340 | def execute(self, context): 341 | 342 | #remember selected uv 343 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 344 | if context.scene.duv_trimcap_uv1 == True: 345 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 346 | uv_trim(context) 347 | if context.scene.duv_trimcap_uv2 == True: 348 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 349 | uv_trim(context) 350 | if context.scene.duv_trimcap_uv1 == False and context.scene.duv_trimcap_uv2 == False: 351 | #just uv selected uv 352 | uv_trim(context) 353 | #reset selected uv 354 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 355 | 356 | if context.scene.duv_autoboxmaptrim == True: 357 | bpy.ops.view3d.dreamuv_uvboxmap() 358 | 359 | return {'FINISHED'} 360 | 361 | def uv_cap(context): 362 | #get atlas first 363 | atlas = read_trim_atlas(context, "cap") 364 | print("atlas:") 365 | print(atlas) 366 | 367 | if context.scene.cap_index > ( len(atlas) - 1.0 ): 368 | context.scene.cap_index = 0.0 369 | 370 | #check if horizontal or vertical, and make trimsheet 371 | 372 | #temp: assuming it's horizontal: 373 | 374 | trimsheet = list() 375 | #for face in atlas: 376 | 377 | 378 | obj = bpy.context.view_layer.objects.active 379 | bm = bmesh.from_edit_mesh(obj.data) 380 | uv_layer = bm.loops.layers.uv.verify() 381 | 382 | HSfaces = list() 383 | #MAKE FACE LIST 384 | for face in bm.faces: 385 | if face.select: 386 | HSfaces.append(face) 387 | 388 | is_rect = DUV_Utils.square_fit(context) 389 | 390 | #FIT TO 0-1 range 391 | xmin, xmax = HSfaces[0].loops[0][uv_layer].uv.x, HSfaces[0].loops[0][uv_layer].uv.x 392 | ymin, ymax = HSfaces[0].loops[0][uv_layer].uv.y, HSfaces[0].loops[0][uv_layer].uv.y 393 | 394 | for face in HSfaces: 395 | for vert in face.loops: 396 | xmin = min(xmin, vert[uv_layer].uv.x) 397 | xmax = max(xmax, vert[uv_layer].uv.x) 398 | ymin = min(ymin, vert[uv_layer].uv.y) 399 | ymax = max(ymax, vert[uv_layer].uv.y) 400 | 401 | #prevent divide by 0: 402 | if (xmax - xmin) == 0: 403 | xmin = .1 404 | if (ymax - ymin) == 0: 405 | ymin = .1 406 | 407 | trimindex = int(context.scene.cap_index) 408 | print("move uv to this:") 409 | print(atlas[trimindex].xmin) 410 | print(atlas[trimindex].ymin) 411 | print(atlas[trimindex].xmax) 412 | print(atlas[trimindex].ymax) 413 | 414 | for face in HSfaces: 415 | for loop in face.loops: 416 | loop[uv_layer].uv.x -= xmin 417 | loop[uv_layer].uv.y -= ymin 418 | loop[uv_layer].uv.x /= (xmax-xmin) 419 | loop[uv_layer].uv.y /= (ymax-ymin) 420 | 421 | #apply the new UV 422 | for face in HSfaces: 423 | for loop in face.loops: 424 | loop[uv_layer].uv.x *= atlas[trimindex].xmax-atlas[trimindex].xmin 425 | loop[uv_layer].uv.y *= atlas[trimindex].ymax-atlas[trimindex].ymin 426 | loop[uv_layer].uv.x += atlas[trimindex].xmin 427 | loop[uv_layer].uv.y += atlas[trimindex].ymin 428 | 429 | 430 | bmesh.update_edit_mesh(obj.data) 431 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 432 | 433 | 434 | class DREAMUV_OT_uv_cap(bpy.types.Operator): 435 | """Unwrap selection as single cap using the trim atlas as a guide""" 436 | bl_idname = "view3d.dreamuv_uvcap" 437 | bl_label = "Cap" 438 | bl_options = {"UNDO"} 439 | 440 | def execute(self, context): 441 | #remember selected uv 442 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 443 | if context.scene.duv_trimcap_uv1 == True: 444 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 445 | uv_cap(context) 446 | if context.scene.duv_trimcap_uv2 == True: 447 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 448 | uv_cap(context) 449 | if context.scene.duv_trimcap_uv1 == False and context.scene.duv_trimcap_uv2 == False: 450 | #just uv selected uv 451 | uv_cap(context) 452 | #reset selected uv 453 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 454 | 455 | if context.scene.duv_autoboxmaptrim == True: 456 | bpy.ops.view3d.dreamuv_uvboxmap() 457 | 458 | return {'FINISHED'} 459 | 460 | def uv_trimnext(self, context): 461 | atlas = read_trim_atlas(context, "trim") 462 | trimindex = int(context.scene.trim_index) 463 | 464 | if self.trimswitched == False: 465 | if self.reverse == False: 466 | context.scene.trim_index += 1 467 | if context.scene.trim_index > ( len(atlas) - 1 ): 468 | context.scene.trim_index = 0 469 | if self.reverse == True: 470 | context.scene.trim_index -= 1 471 | if context.scene.trim_index < 0: 472 | context.scene.trim_index = len(atlas) - 1 473 | 474 | trimindex = int(context.scene.trim_index) 475 | 476 | if bpy.context.object.mode == 'OBJECT': 477 | return {'FINISHED'} 478 | 479 | obj = bpy.context.view_layer.objects.active 480 | bm = bmesh.from_edit_mesh(obj.data) 481 | uv_layer = bm.loops.layers.uv.verify() 482 | 483 | selected_faces = list() 484 | for face in bm.faces: 485 | if face.select: 486 | selected_faces.append(face) 487 | 488 | 489 | if len(selected_faces) == 0: 490 | return {'FINISHED'} 491 | 492 | #select linked uvs 493 | bpy.ops.mesh.select_linked(delimit={'UV'}) 494 | 495 | 496 | faces = list() 497 | #MAKE FACE LIST 498 | for face in bm.faces: 499 | if face.select: 500 | faces.append(face) 501 | 502 | 503 | 504 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 505 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 506 | 507 | for face in faces: 508 | for vert in face.loops: 509 | xmin = min(xmin, vert[uv_layer].uv.x) 510 | xmax = max(xmax, vert[uv_layer].uv.x) 511 | ymin = min(ymin, vert[uv_layer].uv.y) 512 | ymax = max(ymax, vert[uv_layer].uv.y) 513 | 514 | 515 | 516 | width = xmax - xmin 517 | height = ymax - ymin 518 | 519 | 520 | #FIT TO 0-1 range 521 | 522 | #prevent divide by 0: 523 | if (xmax - xmin) == 0: 524 | xmin = .1 525 | if (ymax - ymin) == 0: 526 | ymin = .1 527 | if atlas[0].xmin <= 0.01 and atlas[0].xmax >= 0.99: 528 | for face in faces: 529 | for loop in face.loops: 530 | loop[uv_layer].uv.x -= xmin 531 | loop[uv_layer].uv.y -= ymin 532 | loop[uv_layer].uv.x /= height 533 | loop[uv_layer].uv.y /= height 534 | if atlas[0].ymin <= 0.01 and atlas[0].ymax >= 0.99: 535 | for face in faces: 536 | for loop in face.loops: 537 | loop[uv_layer].uv.x -= xmin 538 | loop[uv_layer].uv.y -= ymin 539 | loop[uv_layer].uv.x /= width 540 | loop[uv_layer].uv.y /= width 541 | 542 | 543 | 544 | if atlas[0].xmin <= 0.01 and atlas[0].xmax >= 0.99: 545 | scale = (atlas[trimindex].ymax-atlas[trimindex].ymin) 546 | #scale the uvs to sheet and move: 547 | for face in faces: 548 | for loop in face.loops: 549 | loop[uv_layer].uv.x *= scale 550 | loop[uv_layer].uv.y *= scale 551 | #move 552 | loop[uv_layer].uv.y += atlas[trimindex].ymin 553 | 554 | #map to bounds: 555 | #first, get current bounds: 556 | bmin = 0 557 | bmax = 0 558 | for face in faces: 559 | for loop in face.loops: 560 | if loop[uv_layer].uv.x > bmax: 561 | bmax = loop[uv_layer].uv.x 562 | 563 | #shift randomly 564 | random_shift = random.random() 565 | if context.scene.duv_uvtrim_randomshift == True: 566 | for face in faces: 567 | for loop in face.loops: 568 | loop[uv_layer].uv.x += random_shift 569 | 570 | #else now scale to bounds: 571 | elif context.scene.duv_uvtrim_bounds == True: 572 | for face in faces: 573 | for loop in face.loops: 574 | loop[uv_layer].uv.x /= bmax 575 | loop[uv_layer].uv.x *= (context.scene.duv_uvtrim_max - context.scene.duv_uvtrim_min) 576 | loop[uv_layer].uv.x += context.scene.duv_uvtrim_min 577 | 578 | if atlas[0].ymin <= 0.01 and atlas[0].ymax >= 0.99: 579 | scale = (atlas[trimindex].xmax-atlas[trimindex].xmin) 580 | #scale the uvs to sheet and move: 581 | for face in faces: 582 | for loop in face.loops: 583 | loop[uv_layer].uv.x *= scale 584 | loop[uv_layer].uv.y *= scale 585 | #move 586 | loop[uv_layer].uv.x += atlas[trimindex].xmin 587 | 588 | #map to bounds: 589 | #first, get current bounds: 590 | bmin = 0 591 | bmax = 0 592 | for face in faces: 593 | for loop in face.loops: 594 | if loop[uv_layer].uv.y > bmax: 595 | bmax = loop[uv_layer].uv.y 596 | 597 | #shift randomly 598 | random_shift = random.random() 599 | if context.scene.duv_uvtrim_randomshift == True: 600 | for face in faces: 601 | for loop in face.loops: 602 | loop[uv_layer].uv.y += random_shift 603 | 604 | #else now scale to bounds: 605 | elif context.scene.duv_uvtrim_bounds == True: 606 | for face in faces: 607 | for loop in face.loops: 608 | loop[uv_layer].uv.y /= bmax 609 | loop[uv_layer].uv.y *= (context.scene.duv_uvtrim_max - context.scene.duv_uvtrim_min) 610 | loop[uv_layer].uv.y += context.scene.duv_uvtrim_min 611 | 612 | #deselect 613 | for face in faces: 614 | face.select = False 615 | #reselect original selected faces 616 | for face in selected_faces: 617 | face.select = True 618 | 619 | bmesh.update_edit_mesh(obj.data) 620 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 621 | 622 | 623 | class DREAMUV_OT_uv_trimnext(bpy.types.Operator): 624 | """Switch selected trim to next one on the atlas sheet""" 625 | bl_idname = "view3d.dreamuv_uvtrimnext" 626 | bl_label = "TrimNext" 627 | bl_options = {"UNDO"} 628 | 629 | reverse : bpy.props.BoolProperty() 630 | trimswitched = False 631 | 632 | def execute(self, context): 633 | 634 | 635 | #remember selected uv 636 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 637 | if context.scene.duv_trimcap_uv1 == True: 638 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 639 | uv_trimnext(self, context) 640 | self.trimswitched = True 641 | if context.scene.duv_trimcap_uv2 == True: 642 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 643 | uv_trimnext(self, context) 644 | if context.scene.duv_trimcap_uv1 == False and context.scene.duv_trimcap_uv2 == False: 645 | #just uv selected uv 646 | uv_trimnext(self, context) 647 | #reset selected uv 648 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 649 | 650 | return {'FINISHED'} 651 | 652 | def uv_capnext(self, context): 653 | atlas = read_trim_atlas(context, "cap") 654 | trimindex = int(context.scene.cap_index) 655 | 656 | if self.trimswitched == False: 657 | if self.reverse == False: 658 | context.scene.cap_index += 1 659 | if context.scene.cap_index > ( len(atlas) - 1 ): 660 | context.scene.cap_index = 0 661 | if self.reverse == True: 662 | context.scene.cap_index -= 1 663 | if context.scene.cap_index < 0: 664 | context.scene.cap_index = len(atlas) - 1 665 | 666 | trimindex = int(context.scene.cap_index) 667 | 668 | if bpy.context.object.mode == 'OBJECT': 669 | return {'FINISHED'} 670 | 671 | obj = bpy.context.view_layer.objects.active 672 | bm = bmesh.from_edit_mesh(obj.data) 673 | uv_layer = bm.loops.layers.uv.verify() 674 | 675 | selected_faces = list() 676 | for face in bm.faces: 677 | if face.select: 678 | selected_faces.append(face) 679 | 680 | 681 | if len(selected_faces) == 0: 682 | return {'FINISHED'} 683 | 684 | #select linked uvs 685 | bpy.ops.mesh.select_linked(delimit={'UV'}) 686 | 687 | faces = list() 688 | #MAKE FACE LIST 689 | for face in bm.faces: 690 | if face.select: 691 | faces.append(face) 692 | 693 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 694 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 695 | 696 | for face in faces: 697 | for vert in face.loops: 698 | xmin = min(xmin, vert[uv_layer].uv.x) 699 | xmax = max(xmax, vert[uv_layer].uv.x) 700 | ymin = min(ymin, vert[uv_layer].uv.y) 701 | ymax = max(ymax, vert[uv_layer].uv.y) 702 | 703 | 704 | 705 | width = xmax - xmin 706 | height = ymax - ymin 707 | 708 | 709 | #FIT TO 0-1 range 710 | 711 | #prevent divide by 0: 712 | if (xmax - xmin) == 0: 713 | xmin = .1 714 | if (ymax - ymin) == 0: 715 | ymin = .1 716 | 717 | for face in faces: 718 | for loop in face.loops: 719 | loop[uv_layer].uv.x -= xmin 720 | loop[uv_layer].uv.y -= ymin 721 | loop[uv_layer].uv.x /= width 722 | loop[uv_layer].uv.y /= height 723 | 724 | 725 | 726 | xscale = (atlas[trimindex].xmax-atlas[trimindex].xmin) 727 | yscale = (atlas[trimindex].ymax-atlas[trimindex].ymin) 728 | 729 | 730 | #scale the uvs to sheet and move: 731 | for face in faces: 732 | for loop in face.loops: 733 | loop[uv_layer].uv.x *= xscale 734 | loop[uv_layer].uv.y *= yscale 735 | #move 736 | loop[uv_layer].uv.x += atlas[trimindex].xmin 737 | loop[uv_layer].uv.y += atlas[trimindex].ymin 738 | 739 | 740 | bmesh.update_edit_mesh(obj.data) 741 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 742 | 743 | class DREAMUV_OT_uv_capnext(bpy.types.Operator): 744 | """Switch selected cap to next one on the atlas sheet""" 745 | bl_idname = "view3d.dreamuv_uvcapnext" 746 | bl_label = "CapNext" 747 | bl_options = {"UNDO"} 748 | 749 | reverse : bpy.props.BoolProperty() 750 | trimswitched = False 751 | 752 | def execute(self, context): 753 | 754 | 755 | #remember selected uv 756 | uv_index = bpy.context.view_layer.objects.active.data.uv_layers.active_index 757 | if context.scene.duv_trimcap_uv1 == True: 758 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 0 759 | uv_capnext(self, context) 760 | self.trimswitched = True 761 | if context.scene.duv_trimcap_uv2 == True: 762 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = 1 763 | uv_capnext(self, context) 764 | if context.scene.duv_trimcap_uv1 == False and context.scene.duv_trimcap_uv2 == False: 765 | #just uv selected uv 766 | uv_capnext(self, context) 767 | #reset selected uv 768 | bpy.context.view_layer.objects.active.data.uv_layers.active_index = uv_index 769 | 770 | 771 | return {'FINISHED'} 772 | 773 | 774 | class trim: 775 | xmin = float() 776 | ymin = float() 777 | xmax = float() 778 | ymax = float() 779 | #rect = list() 780 | horizontal = bool() 781 | 782 | -------------------------------------------------------------------------------- /DUV_UVUnwrap.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | from mathutils import Vector 5 | from . import DUV_Utils 6 | 7 | 8 | class DREAMUV_OT_uv_unwrap_square(bpy.types.Operator): 9 | """Unwrap and attempt to fit to a square shape""" 10 | bl_idname = "view3d.dreamuv_uvunwrapsquare" 11 | bl_label = "unwrap to square shape if possible" 12 | 13 | def execute(self, context): 14 | obj = bpy.context.view_layer.objects.active 15 | bm = bmesh.from_edit_mesh(obj.data) 16 | uv_layer = bm.loops.layers.uv.verify() 17 | 18 | HSfaces = list() 19 | #MAKE FACE LIST 20 | for face in bm.faces: 21 | if face.select: 22 | HSfaces.append(face) 23 | 24 | 25 | is_rect = DUV_Utils.square_fit(context) 26 | 27 | #FIT TO 0-1 range 28 | xmin, xmax = HSfaces[0].loops[0][uv_layer].uv.x, HSfaces[0].loops[0][uv_layer].uv.x 29 | ymin, ymax = HSfaces[0].loops[0][uv_layer].uv.y, HSfaces[0].loops[0][uv_layer].uv.y 30 | 31 | for face in HSfaces: 32 | for vert in face.loops: 33 | xmin = min(xmin, vert[uv_layer].uv.x) 34 | xmax = max(xmax, vert[uv_layer].uv.x) 35 | ymin = min(ymin, vert[uv_layer].uv.y) 36 | ymax = max(ymax, vert[uv_layer].uv.y) 37 | 38 | #prevent divide by 0: 39 | if (xmax - xmin) == 0: 40 | xmin = .1 41 | if (ymax - ymin) == 0: 42 | ymin = .1 43 | 44 | for face in HSfaces: 45 | for loop in face.loops: 46 | loop[uv_layer].uv.x -= xmin 47 | loop[uv_layer].uv.y -= ymin 48 | loop[uv_layer].uv.x /= (xmax-xmin) 49 | loop[uv_layer].uv.y /= (ymax-ymin) 50 | 51 | bmesh.update_edit_mesh(obj.data) 52 | bpy.ops.mesh.select_mode(use_extend=False, use_expand=False, type='FACE') 53 | 54 | return {'FINISHED'} -------------------------------------------------------------------------------- /DUV_Utils.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | import random 5 | from mathutils import Vector 6 | 7 | 8 | def get_face_pixel_step(context, face): 9 | """ 10 | Finds the UV space amount for one pixel of a face, if it is textured 11 | :param context: 12 | :param face: 13 | :return: Vector of the pixel translation, None if face is not textured 14 | """ 15 | # Try to get the material being applied to the face 16 | slot_len = len(context.object.material_slots) 17 | if face.material_index < 0 or face.material_index >= slot_len: 18 | return None 19 | material = context.object.material_slots[face.material_index].material 20 | if material is None: 21 | return None 22 | # Try to get the texture the material is using 23 | target_img = None 24 | for texture_slot in material.texture_slots: 25 | if texture_slot is None: 26 | continue 27 | if texture_slot.texture is None: 28 | continue 29 | if texture_slot.texture.type == 'NONE': 30 | continue 31 | if texture_slot.texture.image is None: 32 | continue 33 | if texture_slot.texture.type == 'IMAGE': 34 | target_img = texture_slot.texture.image 35 | break 36 | if target_img is None: 37 | return None 38 | # With the texture in hand, save the UV step for one pixel movement 39 | pixel_step = Vector((1 / target_img.size[0], 1 / target_img.size[1])) 40 | return pixel_step 41 | 42 | 43 | 44 | 45 | 46 | 47 | def get_orientation(context): 48 | obj = bpy.context.view_layer.objects.active 49 | bm = bmesh.from_edit_mesh(obj.data) 50 | uv_layer = bm.loops.layers.uv.verify() 51 | faces = list() 52 | #MAKE FACE LIST 53 | for face in bm.faces: 54 | if face.select: 55 | faces.append(face) 56 | 57 | for face in faces: 58 | xmin, xmax = face.loops[0][uv_layer].uv.x, face.loops[0][uv_layer].uv.x 59 | ymin, ymax = face.loops[0][uv_layer].uv.y, face.loops[0][uv_layer].uv.y 60 | 61 | for vert in face.loops: 62 | xmin = min(xmin, vert[uv_layer].uv.x) 63 | xmax = max(xmax, vert[uv_layer].uv.x) 64 | ymin = min(ymin, vert[uv_layer].uv.y) 65 | ymax = max(ymax, vert[uv_layer].uv.y) 66 | 67 | # corners: 68 | # 3 2 69 | # 0 1 70 | 71 | bound0 = Vector((xmin,ymin)) 72 | bound1 = Vector((xmax,ymin)) 73 | bound2 = Vector((xmax,ymax)) 74 | bound3 = Vector((xmin,ymax)) 75 | middle = Vector(( ((xmax+xmin)/2)- bound0.x,((ymax+ymin)/2)-bound0.y )) 76 | 77 | distance = middle.length 78 | corner0 = faces[0].loops[0] 79 | for f in faces: 80 | for loop in f.loops: 81 | loop_uv = loop[uv_layer] 82 | vertuv = Vector((loop_uv.uv.x - bound0.x,loop_uv.uv.y - bound0.y)) 83 | tempdistance = vertuv.length 84 | if tempdistance <= distance: 85 | distance = tempdistance 86 | corner0 = loop 87 | 88 | distance = middle.length 89 | corner1 = faces[0].loops[0] 90 | for f in faces: 91 | for loop in f.loops: 92 | loop_uv = loop[uv_layer] 93 | vertuv = Vector((loop_uv.uv.x - bound1.x,loop_uv.uv.y - bound1.y)) 94 | tempdistance = vertuv.length 95 | if tempdistance <= distance: 96 | distance = tempdistance 97 | corner1 = loop 98 | 99 | distance = middle.length 100 | corner2 = faces[0].loops[0] 101 | for f in faces: 102 | for loop in f.loops: 103 | loop_uv = loop[uv_layer] 104 | vertuv = Vector((loop_uv.uv.x - bound2.x,loop_uv.uv.y - bound2.y)) 105 | tempdistance = vertuv.length 106 | if tempdistance <= distance: 107 | distance = tempdistance 108 | corner2 = loop 109 | 110 | distance = middle.length 111 | corner3 = faces[0].loops[0] 112 | for f in faces: 113 | for loop in f.loops: 114 | loop_uv = loop[uv_layer] 115 | vertuv = Vector((loop_uv.uv.x - bound3.x,loop_uv.uv.y - bound3.y)) 116 | tempdistance = vertuv.length 117 | if tempdistance <= distance: 118 | distance = tempdistance 119 | corner3 = loop 120 | 121 | 122 | #orientations: 123 | # 3 2 0 3 1 0 2 1 124 | # 0 1 1 2 2 3 3 0 125 | 126 | #1st case: 127 | #if corner3.vert.co.z >= corner0.vert.co.z and corner2.vert.co.z >= corner1.vert.co.z and corner3.vert.co.z >= corner1.vert.co.z and corner2.vert.co.z >= corner0.vert.co.z: 128 | #print("case1") 129 | 130 | if corner0.vert.co.z >= corner1.vert.co.z and corner3.vert.co.z >= corner2.vert.co.z and corner0.vert.co.z >= corner2.vert.co.z and corner3.vert.co.z >= corner1.vert.co.z: 131 | #print("case2") 132 | for face in faces: 133 | for loop in face.loops: 134 | newx = loop[uv_layer].uv.y 135 | newy = -loop[uv_layer].uv.x 136 | loop[uv_layer].uv.x = newx 137 | loop[uv_layer].uv.y = newy 138 | 139 | if corner1.vert.co.z >= corner2.vert.co.z and corner0.vert.co.z >= corner3.vert.co.z and corner1.vert.co.z >= corner3.vert.co.z and corner0.vert.co.z >= corner2.vert.co.z: 140 | #print("case3") 141 | for face in faces: 142 | for loop in face.loops: 143 | newx = -loop[uv_layer].uv.x 144 | newy = -loop[uv_layer].uv.y 145 | loop[uv_layer].uv.x = newx 146 | loop[uv_layer].uv.y = newy 147 | 148 | if corner2.vert.co.z >= corner3.vert.co.z and corner1.vert.co.z >= corner0.vert.co.z and corner2.vert.co.z >= corner0.vert.co.z and corner1.vert.co.z >= corner3.vert.co.z: 149 | #print("case4") 150 | for face in faces: 151 | for loop in face.loops: 152 | newx = -loop[uv_layer].uv.y 153 | newy = loop[uv_layer].uv.x 154 | loop[uv_layer].uv.x = newx 155 | loop[uv_layer].uv.y = newy 156 | 157 | return None 158 | 159 | 160 | 161 | 162 | 163 | 164 | def get_uv_ratio(context): 165 | #figure out uv size to then compare against subrect size 166 | #to do this I project the mesh using uv coords so i can calculate the area using sum(f.calc_area(). Because I am too lazy to figure out the math 167 | 168 | obj = bpy.context.view_layer.objects.active 169 | bm = bmesh.from_edit_mesh(obj.data) 170 | uv_layer = bm.loops.layers.uv.verify() 171 | faces = list() 172 | #MAKE FACE LIST 173 | for face in bm.faces: 174 | if face.select: 175 | faces.append(face) 176 | backupfaces = list() 177 | for f in faces: 178 | backupface = list() 179 | for loop in f.loops: 180 | loop_uv = loop[uv_layer] 181 | backupvert = list() 182 | backupvert.append(loop.vert.co.x) 183 | backupvert.append(loop.vert.co.y) 184 | backupvert.append(loop.vert.co.z) 185 | backupface.append(backupvert) 186 | backupfaces.append(backupface) 187 | for f in faces: 188 | for loop in f.loops: 189 | loop_uv = loop[uv_layer] 190 | loop.vert.co.xy = loop_uv.uv 191 | loop.vert.co.z = 0 192 | size = area = sum(f.calc_area() for f in faces if f.select) 193 | 194 | #return shape: 195 | for f, backupface in zip(faces, backupfaces): 196 | for loop, backupvert in zip(f.loops, backupface): 197 | loop.vert.co.x = backupvert[0] 198 | loop.vert.co.y = backupvert[1] 199 | loop.vert.co.z = backupvert[2] 200 | #bmesh.update_edit_mesh(obj.data) 201 | return size 202 | 203 | 204 | 205 | 206 | 207 | 208 | def read_atlas(context): 209 | atlas = list() 210 | obj = context.scene.subrect_atlas 211 | me = obj.data 212 | bm = bmesh.new() 213 | bm.from_mesh(me) 214 | uv_layer = bm.loops.layers.uv.verify() 215 | #lets read coords 216 | 217 | faces = list() 218 | 219 | #MAKE FACE LIST 220 | for face in bm.faces: 221 | faces.append(face) 222 | 223 | for face in faces: 224 | xmin, xmax = face.loops[0][uv_layer].uv.x, face.loops[0][uv_layer].uv.x 225 | ymin, ymax = face.loops[0][uv_layer].uv.y, face.loops[0][uv_layer].uv.y 226 | 227 | for vert in face.loops: 228 | xmin = min(xmin, vert[uv_layer].uv.x) 229 | xmax = max(xmax, vert[uv_layer].uv.x) 230 | ymin = min(ymin, vert[uv_layer].uv.y) 231 | ymax = max(ymax, vert[uv_layer].uv.y) 232 | 233 | new_subrect = subrect() 234 | edge1 = xmax - xmin 235 | edge2 = ymax - ymin 236 | 237 | 238 | rect = list() 239 | 240 | for loop in face.loops: 241 | loop_uv = loop[uv_layer] 242 | #make sure to create new uv vector here, dont reference 243 | uvcoord = Vector((loop_uv.uv.x,loop_uv.uv.y)) 244 | rect.append(uvcoord) 245 | 246 | new_subrect.uvcoord = rect 247 | 248 | #calculate aspect ratio 249 | if edge1 > 0 and edge2 > 0: 250 | 251 | aspect = edge1/edge2 252 | if aspect > 1: 253 | aspect = round(aspect) 254 | else: 255 | aspect = 1/(round(1/aspect)) 256 | #aspect = 1/aspect 257 | posaspect = aspect 258 | if posaspect < 1.0: 259 | posaspect = 1/posaspect 260 | #calculate size 261 | size = face.calc_area() 262 | 263 | #adjust scale 264 | size /= context.scene.duvhotspotscale*context.scene.duvhotspotscale 265 | 266 | size = float('%.2g' % size) #round to 2 significant digits 267 | 268 | 269 | 270 | 271 | new_subrect.aspect = aspect 272 | new_subrect.posaspect = posaspect 273 | new_subrect.size = size 274 | atlas.append(new_subrect) 275 | 276 | return atlas 277 | 278 | 279 | 280 | 281 | 282 | 283 | def donut_uv_fixer(context): 284 | obj = bpy.context.view_layer.objects.active 285 | bm = bmesh.from_edit_mesh(obj.data) 286 | uv_layer = bm.loops.layers.uv.verify() 287 | 288 | faces = list() 289 | 290 | #MAKE FACE LIST 291 | for face in bm.faces: 292 | if face.select: 293 | faces.append(face) 294 | 295 | #Unwrap and get the edge verts 296 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 297 | bpy.ops.mesh.region_to_loop() 298 | 299 | edge_list = list() 300 | for e in bm.edges: 301 | if e.select is True: 302 | edge_list.append(e) 303 | #print(e) 304 | 305 | #select faces again 306 | for f in faces: 307 | f.select = True 308 | #get start loop (this makes sure we loop in the right direction) 309 | startloop = None 310 | 311 | for l in edge_list[0].link_loops: 312 | if l.face.select is True: 313 | startloop = l 314 | #create sorted verts from start loop 315 | sorted_vert_list = list() 316 | for f in faces: 317 | f.select = False 318 | for e in edge_list: 319 | e.select = True 320 | 321 | sorted_vert_list.append(startloop.vert) 322 | startloop.edge.select = False 323 | sorted_vert_list.append(startloop.link_loop_next.vert) 324 | 325 | print("CHECKING DOUNt!!!!") 326 | for i in range(1,len(edge_list)-1): 327 | #catch if a patch is donut shaped: 328 | if i >= len(sorted_vert_list): 329 | for f in faces: 330 | f.select = True 331 | bmesh.update_edit_mesh(obj.data) 332 | return False 333 | 334 | for e in sorted_vert_list[i].link_edges: 335 | if e.select is True: 336 | sorted_vert_list.append(e.other_vert(sorted_vert_list[i])) 337 | e.select = False 338 | 339 | #reselect faces: 340 | for f in faces: 341 | f.select = True 342 | 343 | return True 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | def square_fit(context): 352 | 353 | 354 | #return {'FINISHED'} 355 | 356 | obj = bpy.context.view_layer.objects.active 357 | bm = bmesh.from_edit_mesh(obj.data) 358 | 359 | uv_layer = bm.loops.layers.uv.verify() 360 | 361 | faces = list() 362 | 363 | #MAKE FACE LIST 364 | for face in bm.faces: 365 | if face.select: 366 | faces.append(face) 367 | 368 | #TEST IF QUADS OR NOT 369 | quadmethod = True 370 | #EXPERIMENTAL! TO MUCH SKEWING TEST: 371 | distorted = False 372 | 373 | for face in faces: 374 | if len(face.loops) != 4 : 375 | quadmethod = False 376 | 377 | #FIRST FIX DONUT SHAPES: 378 | noDonut = True 379 | noDonut = donut_uv_fixer(context) 380 | if noDonut is False: 381 | #select boundary edges 382 | bpy.ops.mesh.region_to_loop() 383 | boundary_edge_list = list() 384 | for e in bm.edges: 385 | if e.select is True: 386 | boundary_edge_list.append(e) 387 | 388 | #pick a random edge for where the topology cut will start 389 | #active_edge = boundary_edge_list[0] 390 | active_edge = boundary_edge_list[random.randint(0, len(boundary_edge_list)-1)] 391 | bm.select_history.add(active_edge) 392 | 393 | #if its all quads, we can probably just cut it straight 394 | if quadmethod: 395 | for l in active_edge.verts[0].link_edges: 396 | if l.select == False: 397 | l.select = True 398 | bm.select_history.add(l) 399 | break 400 | 401 | bpy.ops.mesh.loop_multi_select(ring=False) 402 | 403 | 404 | else: 405 | #walk through the boundary where the active edge exists to deselect them without deselecting the other boundary 406 | currentvert = active_edge.verts[0] 407 | foundedge = True 408 | while foundedge == True: 409 | foundedge = False 410 | for le in currentvert.link_edges: 411 | if le.select == True: 412 | currentvert = le.other_vert(currentvert) 413 | le.select = False 414 | foundedge = True 415 | break 416 | 417 | 418 | #Dijkstra version 419 | #1)make list of all verts for distance values: 420 | Dverts = list() 421 | for v in bm.verts: 422 | Dverts.append(0) 423 | 424 | #set first index 425 | active_edge.select=False 426 | Dverts[active_edge.verts[0].index] = 1 427 | startvert = active_edge.verts[0].index 428 | 429 | #this will be a loop 430 | endvert = 0 431 | currentStep = 1 432 | iterating = True 433 | while iterating == True: 434 | for v in bm.verts: 435 | if Dverts[v.index] == currentStep: 436 | for l in v.link_edges: 437 | if Dverts[l.other_vert(v).index] == 0: 438 | Dverts[l.other_vert(v).index] = currentStep + 1 439 | if l.other_vert(v).select == True: 440 | iterating = False 441 | endvert = l.other_vert(v).index 442 | break 443 | currentStep += 1 444 | 445 | #now we have a start and end vert, just run shortest path between them for quick selection 446 | for v in bm.verts: 447 | v.select = False 448 | for e in boundary_edge_list: 449 | e.select = False 450 | bm.verts.ensure_lookup_table() 451 | bm.verts[startvert].select=True 452 | bm.verts[endvert].select=True 453 | 454 | bpy.ops.mesh.select_mode(type="VERT") 455 | 456 | #if shortest path is more than one edge use: 457 | shortconnection = False 458 | for l in bm.verts[startvert].link_edges: 459 | if l.select == True: 460 | shortconnection = True 461 | if shortconnection == False: 462 | bpy.ops.mesh.shortest_path_select(edge_mode='SELECT') 463 | 464 | bpy.ops.mesh.select_mode(type="EDGE") 465 | 466 | #turn into seam and split 467 | bpy.ops.mesh.mark_seam(clear=False) 468 | bpy.ops.mesh.edge_split() 469 | 470 | 471 | #reset selection 472 | for f in faces: 473 | f.select = True 474 | 475 | 476 | 477 | #SLOW HERE, find faster way to test if selection is ring shaped 478 | 479 | #Unwrap and get the edge verts 480 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 481 | bpy.ops.mesh.region_to_loop() 482 | 483 | edge_list = list() 484 | for e in bm.edges: 485 | if e.select is True: 486 | edge_list.append(e) 487 | #print(e) 488 | 489 | #select faces again 490 | for f in faces: 491 | f.select = True 492 | #get start loop (this makes sure we loop in the right direction) 493 | startloop = None 494 | 495 | if(len(edge_list) == 0): 496 | #print("weird! - means no mesh was sent?") 497 | return distorted 498 | 499 | for l in edge_list[0].link_loops: 500 | if l.face.select is True: 501 | startloop = l 502 | #create sorted verts from start loop 503 | sorted_vert_list = list() 504 | for f in faces: 505 | f.select = False 506 | for e in edge_list: 507 | e.select = True 508 | 509 | sorted_vert_list.append(startloop.vert) 510 | startloop.edge.select = False 511 | sorted_vert_list.append(startloop.link_loop_next.vert) 512 | 513 | for i in range(1,len(edge_list)-1): 514 | #catch again if a patch is donut shaped: 515 | if i >= len(sorted_vert_list): 516 | for f in faces: 517 | f.select = True 518 | bmesh.update_edit_mesh(obj.data) 519 | #print("DONUT PATCH!!!!") 520 | return False 521 | 522 | for e in sorted_vert_list[i].link_edges: 523 | if e.select is True: 524 | sorted_vert_list.append(e.other_vert(sorted_vert_list[i])) 525 | e.select = False 526 | 527 | #select faces again 528 | for f in faces: 529 | f.select = True 530 | 531 | #get UV 532 | sorted_uv_list = list() 533 | uv_layer = bm.loops.layers.uv.active 534 | for v in sorted_vert_list: 535 | for l in v.link_loops: 536 | if l.face.select is True: 537 | sorted_uv_list.append(l[uv_layer]) 538 | break 539 | 540 | #get all angles 541 | sorted_angle_list = list() 542 | 543 | for i in range(len(sorted_uv_list)): 544 | prev = (i-1)%len(sorted_uv_list) 545 | next = (i+1)%len(sorted_uv_list) 546 | vector1 = Vector((sorted_uv_list[prev].uv.y-sorted_uv_list[i].uv.y,sorted_uv_list[prev].uv.x-sorted_uv_list[i].uv.x)) 547 | vector2 = Vector((sorted_uv_list[next].uv.y-sorted_uv_list[i].uv.y,sorted_uv_list[next].uv.x-sorted_uv_list[i].uv.x)) 548 | #check failcase of zero length vector: 549 | if vector1.length == 0 or vector2.length == 0: 550 | bmesh.update_edit_mesh(obj.data) 551 | return False 552 | angle = -math.degrees(vector1.angle_signed(vector2)) 553 | if angle < 0: 554 | angle += 360 555 | sorted_angle_list.append(angle) 556 | 557 | 558 | #find concaves: 559 | for i in range(len(sorted_angle_list)): 560 | if sorted_angle_list[i] > 230: 561 | distorted = True 562 | bmesh.update_edit_mesh(obj.data) 563 | return False 564 | 565 | #angle test: 566 | #test if more than 4 90 degrees: 567 | NCount = 0 568 | for i in range(len(sorted_angle_list)): 569 | if sorted_angle_list[i] < 100: 570 | NCount += 1 571 | if NCount > 4: 572 | distorted = True 573 | 574 | #now find top 4 angles 575 | topangles = list() 576 | for o in range(4): 577 | top = 360 578 | topindex = -1 579 | for i in range(len(sorted_angle_list)): 580 | if sorted_angle_list[i] < top: 581 | top = sorted_angle_list[i] 582 | topindex = i 583 | if o == 3: 584 | if sorted_angle_list[topindex] > 125: 585 | distorted = True 586 | 587 | topangles.append(topindex) 588 | sorted_angle_list[topindex] = 999 #lol 589 | 590 | sorted_corner_list = list() 591 | for i in range(len(sorted_uv_list)): 592 | sorted_corner_list.append(False) 593 | sorted_corner_list[topangles[0]] = True 594 | sorted_corner_list[topangles[1]] = True 595 | sorted_corner_list[topangles[2]] = True 596 | sorted_corner_list[topangles[3]] = True 597 | 598 | #find bottom left corner (using distance method seems to work well) 599 | distance = 2 600 | closest = 0 601 | for t in topangles: 602 | l = sorted_uv_list[t].uv.length 603 | if l < distance: 604 | distance = l 605 | closest = t 606 | 607 | #rotate lists to get clostest corner at start: 608 | for i in range(closest): 609 | sorted_corner_list.append(sorted_corner_list.pop(0)) 610 | sorted_uv_list.append(sorted_uv_list.pop(0)) 611 | sorted_vert_list.append(sorted_vert_list.pop(0)) 612 | 613 | #create coord list: 614 | cornerz = list() 615 | 616 | for i in range(len(sorted_vert_list)): 617 | if sorted_corner_list[i] is True: 618 | cornerz.append(sorted_vert_list[i].co.z) 619 | 620 | sorted_edge_ratios = list() 621 | 622 | #get edge lenghts 623 | edge = list() 624 | for i in range(len(sorted_vert_list)): 625 | if sorted_corner_list[i] is True: 626 | sorted_edge_ratios.append(0) 627 | if i != 0: 628 | l = (sorted_vert_list[i-1].co.xyz - sorted_vert_list[i].co.xyz).length 629 | edge.append(sorted_edge_ratios[i-1] + l) 630 | 631 | if sorted_corner_list[i] is False: 632 | l = (sorted_vert_list[i-1].co.xyz - sorted_vert_list[i].co.xyz).length 633 | sorted_edge_ratios.append(sorted_edge_ratios[i-1] + l) 634 | if i is (len(sorted_vert_list)-1): 635 | l = (sorted_vert_list[i].co.xyz - sorted_vert_list[0].co.xyz).length 636 | edge.append(sorted_edge_ratios[i] + l) 637 | 638 | 639 | if quadmethod: 640 | #MAP FIRST QUAD 641 | edge1 = (faces[0].loops[0].vert.co.xyz - faces[0].loops[1].vert.co.xyz).length 642 | edge2 = (faces[0].loops[1].vert.co.xyz - faces[0].loops[2].vert.co.xyz).length 643 | 644 | faces[0].loops[0][uv_layer].uv.x = 0 645 | faces[0].loops[0][uv_layer].uv.y = 0 646 | faces[0].loops[1][uv_layer].uv.x = edge1 647 | faces[0].loops[1][uv_layer].uv.y = 0 648 | faces[0].loops[2][uv_layer].uv.x = edge1 649 | faces[0].loops[2][uv_layer].uv.y = edge2 650 | faces[0].loops[3][uv_layer].uv.x = 0 651 | faces[0].loops[3][uv_layer].uv.y = edge2 652 | 653 | bm.faces.active = faces[0] 654 | 655 | #UNWRAP ADJACENT 656 | bpy.ops.uv.follow_active_quads() 657 | uv_layer = bm.loops.layers.uv.verify() 658 | 659 | #return 660 | edge1 = (edge[0]+edge[2])*.5 661 | edge2 = (edge[1]+edge[3])*.5 662 | 663 | #FIT TO 0-1 range 664 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 665 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 666 | 667 | for face in faces: 668 | for vert in face.loops: 669 | xmin = min(xmin, vert[uv_layer].uv.x) 670 | xmax = max(xmax, vert[uv_layer].uv.x) 671 | ymin = min(ymin, vert[uv_layer].uv.y) 672 | ymax = max(ymax, vert[uv_layer].uv.y) 673 | 674 | #return 675 | 676 | #prevent divide by 0: 677 | if (xmax - xmin) == 0: 678 | xmin = .1 679 | if (ymax - ymin) == 0: 680 | ymin = .1 681 | 682 | for face in faces: 683 | for loop in face.loops: 684 | loop[uv_layer].uv.x -= xmin 685 | loop[uv_layer].uv.y -= ymin 686 | loop[uv_layer].uv.x /= (xmax-xmin) 687 | loop[uv_layer].uv.y /= (ymax-ymin) 688 | 689 | #shift extents to be positive only: 690 | xmax = xmax - xmin 691 | ymax = ymax - ymin 692 | 693 | #now fit to correct edge lengths: 694 | if (edge1 < edge2): 695 | #flip them: 696 | tedge = edge1 697 | edge1 = edge2 698 | edge2 = tedge 699 | 700 | if xmax >= ymax: 701 | for face in faces: 702 | for loop in face.loops: 703 | loop[uv_layer].uv.x *= edge1 704 | loop[uv_layer].uv.y *= edge2 705 | if xmax < ymax: 706 | for face in faces: 707 | for loop in face.loops: 708 | loop[uv_layer].uv.x *= edge2 709 | loop[uv_layer].uv.y *= edge1 710 | 711 | if quadmethod is False: 712 | if distorted is False: 713 | #NOW LAY OUT ALL EDGE UVs 714 | i = 0 715 | #EDGE 1 716 | for l in sorted_vert_list[i].link_loops: 717 | if l.face.select is True: 718 | l[uv_layer].uv = Vector((0,0)) 719 | i += 1 720 | while sorted_corner_list[i] is False: 721 | for l in sorted_vert_list[i].link_loops: 722 | if l.face.select is True: 723 | l[uv_layer].uv = Vector((sorted_edge_ratios[i]/edge[0],0)) 724 | i += 1 725 | #EDGE 2 726 | for l in sorted_vert_list[i].link_loops: 727 | if l.face.select is True: 728 | l[uv_layer].uv = Vector((1,0)) 729 | i += 1 730 | while sorted_corner_list[i] is False: 731 | for l in sorted_vert_list[i].link_loops: 732 | if l.face.select is True: 733 | l[uv_layer].uv = Vector((1,sorted_edge_ratios[i]/edge[1])) 734 | i += 1 735 | #EDGE 3 736 | for l in sorted_vert_list[i].link_loops: 737 | if l.face.select is True: 738 | l[uv_layer].uv = Vector((1,1)) 739 | i += 1 740 | while sorted_corner_list[i] is False: 741 | for l in sorted_vert_list[i].link_loops: 742 | if l.face.select is True: 743 | l[uv_layer].uv = Vector((1-(sorted_edge_ratios[i]/edge[2]),1)) 744 | i += 1 745 | #EDGE 4 746 | for l in sorted_vert_list[i].link_loops: 747 | if l.face.select is True: 748 | l[uv_layer].uv = Vector((0,1)) 749 | i += 1 750 | for o in range(i,len(sorted_vert_list)): 751 | for l in sorted_vert_list[o].link_loops: 752 | if l.face.select is True: 753 | l[uv_layer].uv = Vector((0,1-(sorted_edge_ratios[o]/edge[3]))) 754 | 755 | #set proper aspect ratio 756 | 757 | for f in bm.faces: 758 | if f.select is True: 759 | for loop in f.loops: 760 | loop_uv = loop[uv_layer] 761 | loop_uv.uv.x *= edge[0] 762 | loop_uv.uv.y *= edge[1] 763 | 764 | 765 | #newmethod: 766 | #select boundary and pin 767 | bpy.ops.uv.select_all(action='DESELECT') 768 | for v in sorted_vert_list: 769 | for l in v.link_loops: 770 | l[uv_layer].select = True 771 | bpy.ops.uv.pin(clear=False) 772 | 773 | #select all and unwrap (and unpin?) 774 | bpy.ops.uv.select_all(action='SELECT') 775 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 776 | bpy.ops.uv.pin(clear=True) 777 | 778 | #return False 779 | #select boundary and unpin 780 | 781 | #bpy.ops.uv.select_all(action='SELECT') 782 | #expand middle verts 783 | #bpy.ops.uv.minimize_stretch(iterations=50) 784 | #return true if rect fit was succesful 785 | 786 | return not distorted 787 | 788 | 789 | 790 | 791 | 792 | 793 | class subrect: 794 | aspect = int() 795 | posaspect = int() 796 | size = float() 797 | uvcoord = list() 798 | 799 | class trim: 800 | uvcoord = list() 801 | 802 | 803 | -------------------------------------------------------------------------------- /DUV_Utils_backup.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | import math 4 | from mathutils import Vector 5 | 6 | 7 | def get_face_pixel_step(context, face): 8 | """ 9 | Finds the UV space amount for one pixel of a face, if it is textured 10 | :param context: 11 | :param face: 12 | :return: Vector of the pixel translation, None if face is not textured 13 | """ 14 | # Try to get the material being applied to the face 15 | slot_len = len(context.object.material_slots) 16 | if face.material_index < 0 or face.material_index >= slot_len: 17 | return None 18 | material = context.object.material_slots[face.material_index].material 19 | if material is None: 20 | return None 21 | # Try to get the texture the material is using 22 | target_img = None 23 | for texture_slot in material.texture_slots: 24 | if texture_slot is None: 25 | continue 26 | if texture_slot.texture is None: 27 | continue 28 | if texture_slot.texture.type == 'NONE': 29 | continue 30 | if texture_slot.texture.image is None: 31 | continue 32 | if texture_slot.texture.type == 'IMAGE': 33 | target_img = texture_slot.texture.image 34 | break 35 | if target_img is None: 36 | return None 37 | # With the texture in hand, save the UV step for one pixel movement 38 | pixel_step = Vector((1 / target_img.size[0], 1 / target_img.size[1])) 39 | return pixel_step 40 | 41 | def get_orientation(context): 42 | obj = bpy.context.view_layer.objects.active 43 | bm = bmesh.from_edit_mesh(obj.data) 44 | uv_layer = bm.loops.layers.uv.verify() 45 | faces = list() 46 | #MAKE FACE LIST 47 | for face in bm.faces: 48 | if face.select: 49 | faces.append(face) 50 | 51 | for face in faces: 52 | xmin, xmax = face.loops[0][uv_layer].uv.x, face.loops[0][uv_layer].uv.x 53 | ymin, ymax = face.loops[0][uv_layer].uv.y, face.loops[0][uv_layer].uv.y 54 | 55 | for vert in face.loops: 56 | xmin = min(xmin, vert[uv_layer].uv.x) 57 | xmax = max(xmax, vert[uv_layer].uv.x) 58 | ymin = min(ymin, vert[uv_layer].uv.y) 59 | ymax = max(ymax, vert[uv_layer].uv.y) 60 | 61 | # corners: 62 | # 3 2 63 | # 0 1 64 | 65 | bound0 = Vector((xmin,ymin)) 66 | bound1 = Vector((xmax,ymin)) 67 | bound2 = Vector((xmax,ymax)) 68 | bound3 = Vector((xmin,ymax)) 69 | middle = Vector(( ((xmax+xmin)/2)- bound0.x,((ymax+ymin)/2)-bound0.y )) 70 | 71 | distance = middle.length 72 | corner0 = faces[0].loops[0] 73 | for f in faces: 74 | for loop in f.loops: 75 | loop_uv = loop[uv_layer] 76 | vertuv = Vector((loop_uv.uv.x - bound0.x,loop_uv.uv.y - bound0.y)) 77 | tempdistance = vertuv.length 78 | if tempdistance <= distance: 79 | distance = tempdistance 80 | corner0 = loop 81 | 82 | distance = middle.length 83 | corner1 = faces[0].loops[0] 84 | for f in faces: 85 | for loop in f.loops: 86 | loop_uv = loop[uv_layer] 87 | vertuv = Vector((loop_uv.uv.x - bound1.x,loop_uv.uv.y - bound1.y)) 88 | tempdistance = vertuv.length 89 | if tempdistance <= distance: 90 | distance = tempdistance 91 | corner1 = loop 92 | 93 | distance = middle.length 94 | corner2 = faces[0].loops[0] 95 | for f in faces: 96 | for loop in f.loops: 97 | loop_uv = loop[uv_layer] 98 | vertuv = Vector((loop_uv.uv.x - bound2.x,loop_uv.uv.y - bound2.y)) 99 | tempdistance = vertuv.length 100 | if tempdistance <= distance: 101 | distance = tempdistance 102 | corner2 = loop 103 | 104 | distance = middle.length 105 | corner3 = faces[0].loops[0] 106 | for f in faces: 107 | for loop in f.loops: 108 | loop_uv = loop[uv_layer] 109 | vertuv = Vector((loop_uv.uv.x - bound3.x,loop_uv.uv.y - bound3.y)) 110 | tempdistance = vertuv.length 111 | if tempdistance <= distance: 112 | distance = tempdistance 113 | corner3 = loop 114 | 115 | 116 | #orientations: 117 | # 3 2 0 3 1 0 2 1 118 | # 0 1 1 2 2 3 3 0 119 | 120 | #1st case: 121 | #if corner3.vert.co.z >= corner0.vert.co.z and corner2.vert.co.z >= corner1.vert.co.z and corner3.vert.co.z >= corner1.vert.co.z and corner2.vert.co.z >= corner0.vert.co.z: 122 | #print("case1") 123 | 124 | if corner0.vert.co.z >= corner1.vert.co.z and corner3.vert.co.z >= corner2.vert.co.z and corner0.vert.co.z >= corner2.vert.co.z and corner3.vert.co.z >= corner1.vert.co.z: 125 | #print("case2") 126 | for face in faces: 127 | for loop in face.loops: 128 | newx = loop[uv_layer].uv.y 129 | newy = -loop[uv_layer].uv.x 130 | loop[uv_layer].uv.x = newx 131 | loop[uv_layer].uv.y = newy 132 | 133 | if corner1.vert.co.z >= corner2.vert.co.z and corner0.vert.co.z >= corner3.vert.co.z and corner1.vert.co.z >= corner3.vert.co.z and corner0.vert.co.z >= corner2.vert.co.z: 134 | #print("case3") 135 | for face in faces: 136 | for loop in face.loops: 137 | newx = -loop[uv_layer].uv.x 138 | newy = -loop[uv_layer].uv.y 139 | loop[uv_layer].uv.x = newx 140 | loop[uv_layer].uv.y = newy 141 | 142 | if corner2.vert.co.z >= corner3.vert.co.z and corner1.vert.co.z >= corner0.vert.co.z and corner2.vert.co.z >= corner0.vert.co.z and corner1.vert.co.z >= corner3.vert.co.z: 143 | #print("case4") 144 | for face in faces: 145 | for loop in face.loops: 146 | newx = -loop[uv_layer].uv.y 147 | newy = loop[uv_layer].uv.x 148 | loop[uv_layer].uv.x = newx 149 | loop[uv_layer].uv.y = newy 150 | 151 | return None 152 | 153 | 154 | def get_uv_ratio(context): 155 | #figure out uv size to then compare against subrect size 156 | #to do this I project the mesh using uv coords so i can calculate the area using sum(f.calc_area(). Because I am too lazy to figure out the math 157 | #this code is terrible, someone who knows math make this better, thanks 158 | obj = bpy.context.view_layer.objects.active 159 | bm = bmesh.from_edit_mesh(obj.data) 160 | uv_layer = bm.loops.layers.uv.verify() 161 | faces = list() 162 | #MAKE FACE LIST 163 | for face in bm.faces: 164 | if face.select: 165 | faces.append(face) 166 | backupfaces = list() 167 | for f in faces: 168 | backupface = list() 169 | for loop in f.loops: 170 | loop_uv = loop[uv_layer] 171 | backupvert = list() 172 | backupvert.append(loop.vert.co.x) 173 | backupvert.append(loop.vert.co.y) 174 | backupvert.append(loop.vert.co.z) 175 | backupface.append(backupvert) 176 | backupfaces.append(backupface) 177 | for f in faces: 178 | for loop in f.loops: 179 | loop_uv = loop[uv_layer] 180 | loop.vert.co.xy = loop_uv.uv 181 | loop.vert.co.z = 0 182 | size = area = sum(f.calc_area() for f in faces if f.select) 183 | 184 | #return shape: 185 | for f, backupface in zip(faces, backupfaces): 186 | for loop, backupvert in zip(f.loops, backupface): 187 | loop.vert.co.x = backupvert[0] 188 | loop.vert.co.y = backupvert[1] 189 | loop.vert.co.z = backupvert[2] 190 | #bmesh.update_edit_mesh(obj.data) 191 | return size 192 | 193 | def read_atlas(context): 194 | atlas = list() 195 | obj = context.scene.subrect_atlas 196 | me = obj.data 197 | bm = bmesh.new() 198 | bm.from_mesh(me) 199 | uv_layer = bm.loops.layers.uv.verify() 200 | #lets read coords 201 | 202 | faces = list() 203 | 204 | #MAKE FACE LIST 205 | for face in bm.faces: 206 | faces.append(face) 207 | 208 | for face in faces: 209 | xmin, xmax = face.loops[0][uv_layer].uv.x, face.loops[0][uv_layer].uv.x 210 | ymin, ymax = face.loops[0][uv_layer].uv.y, face.loops[0][uv_layer].uv.y 211 | 212 | for vert in face.loops: 213 | xmin = min(xmin, vert[uv_layer].uv.x) 214 | xmax = max(xmax, vert[uv_layer].uv.x) 215 | ymin = min(ymin, vert[uv_layer].uv.y) 216 | ymax = max(ymax, vert[uv_layer].uv.y) 217 | 218 | new_subrect = subrect() 219 | edge1 = xmax - xmin 220 | edge2 = ymax - ymin 221 | 222 | 223 | rect = list() 224 | 225 | for loop in face.loops: 226 | loop_uv = loop[uv_layer] 227 | #make sure to create new uv vector here, dont reference 228 | uvcoord = Vector((loop_uv.uv.x,loop_uv.uv.y)) 229 | rect.append(uvcoord) 230 | 231 | new_subrect.uvcoord = rect 232 | 233 | #calculate aspect ratio 234 | if edge1 > 0 and edge2 > 0: 235 | 236 | aspect = edge1/edge2 237 | if aspect > 1: 238 | aspect = round(aspect) 239 | else: 240 | aspect = 1/(round(1/aspect)) 241 | #aspect = 1/aspect 242 | posaspect = aspect 243 | if posaspect < 1.0: 244 | posaspect = 1/posaspect 245 | #calculate size 246 | size = face.calc_area() 247 | 248 | #adjust scale 249 | size /= context.scene.duvhotspotscale*context.scene.duvhotspotscale 250 | 251 | size = float('%.2g' % size) #round to 2 significant digits 252 | 253 | 254 | 255 | 256 | new_subrect.aspect = aspect 257 | new_subrect.posaspect = posaspect 258 | new_subrect.size = size 259 | atlas.append(new_subrect) 260 | 261 | #print("atlas") 262 | #print(atlas) 263 | #for a in atlas: 264 | # print(a.posaspect) 265 | return atlas 266 | 267 | 268 | 269 | def square_fit(context): 270 | obj = bpy.context.view_layer.objects.active 271 | bm = bmesh.from_edit_mesh(obj.data) 272 | 273 | uv_layer = bm.loops.layers.uv.verify() 274 | 275 | faces = list() 276 | 277 | #MAKE FACE LIST 278 | for face in bm.faces: 279 | if face.select: 280 | faces.append(face) 281 | 282 | #TEST IF QUADS OR NOT 283 | 284 | quadmethod = True 285 | #EXPERIMENTAL! TO MUCH SKEWING TEST: 286 | distorted = False 287 | 288 | for face in faces: 289 | if len(face.loops) != 4 : 290 | quadmethod = False 291 | print('no quad!') 292 | if quadmethod: 293 | print('quad!') 294 | 295 | #SLOW HERE, find faster way to test if selection is ring shaped 296 | 297 | #Unwrap and get the edge verts 298 | bmesh.update_edit_mesh(obj.data) 299 | bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) 300 | bpy.ops.mesh.region_to_loop() 301 | 302 | obj = bpy.context.edit_object 303 | me = obj.data 304 | bm = bmesh.from_edit_mesh(me) 305 | 306 | edge_list = list() 307 | for e in bm.edges: 308 | if e.select is True and e.seam is False: 309 | edge_list.append(e) 310 | #print(e) 311 | 312 | #print("edge list") 313 | #print(edge_list) 314 | 315 | #select faces again 316 | for f in faces: 317 | f.select = True 318 | #get start loop (this makes sure we loop in the right direction) 319 | 320 | #add seams to edge list also? 321 | for e in bm.edges: 322 | if e.seam is True: 323 | print("seam") 324 | for f in e.link_faces: 325 | print("seam faces:") 326 | print(f.select) 327 | if f.select is True: 328 | print("adding seam") 329 | edge_list.append(e) 330 | 331 | 332 | startloop = None 333 | 334 | if(len(edge_list) == 0): 335 | print("weird! - means no mesh was sent?") 336 | return distorted 337 | 338 | for l in edge_list[0].link_loops: 339 | if l.face.select is True: 340 | startloop = l 341 | #create sorted verts from start loop 342 | sorted_vert_list = list() 343 | for f in faces: 344 | f.select = False 345 | for e in edge_list: 346 | e.select = True 347 | 348 | 349 | 350 | sorted_vert_list.append(startloop.vert) 351 | startloop.edge.select = False 352 | sorted_vert_list.append(startloop.link_loop_next.vert) 353 | 354 | #for i in range(1,len(edge_list)-1): 355 | 356 | #catch if a patch is donut shaped: 357 | #if i >= len(sorted_vert_list): 358 | # for f in faces: 359 | # f.select = True 360 | # bmesh.update_edit_mesh(obj.data) 361 | # print("donut shape") 362 | # break 363 | 364 | #note: make it take a turn here when finding a seam! 365 | 366 | 367 | # for e in sorted_vert_list[i].link_edges: 368 | # if e.select is True: 369 | # sorted_vert_list.append(e.other_vert(sorted_vert_list[i])) 370 | # e.select = False 371 | 372 | nextvert = startloop.link_loop_next.vert 373 | i = 1 374 | while sorted_vert_list[i] is not startloop.vert or i == 500: 375 | 376 | 377 | 378 | #note: make it take a turn here when finding a seam! 379 | for e in sorted_vert_list[i].link_edges: 380 | if e.select is True: 381 | sorted_vert_list.append(e.other_vert(sorted_vert_list[i])) 382 | e.select = False 383 | 384 | 385 | 386 | 387 | i += 1 388 | sorted_vert_list.pop() #remove last item which is the same as the first anyways 389 | print(sorted_vert_list) 390 | 391 | 392 | 393 | 394 | 395 | #select faces again 396 | for f in faces: 397 | f.select = True 398 | 399 | #get UV 400 | sorted_uv_list = list() 401 | uv_layer = bm.loops.layers.uv.active 402 | for v in sorted_vert_list: 403 | for l in v.link_loops: 404 | if l.face.select is True: 405 | sorted_uv_list.append(l[uv_layer]) 406 | break 407 | 408 | #get all angles 409 | sorted_angle_list = list() 410 | 411 | for i in range(len(sorted_uv_list)): 412 | prev = (i-1)%len(sorted_uv_list) 413 | next = (i+1)%len(sorted_uv_list) 414 | vector1 = Vector((sorted_uv_list[prev].uv.y-sorted_uv_list[i].uv.y,sorted_uv_list[prev].uv.x-sorted_uv_list[i].uv.x)) 415 | vector2 = Vector((sorted_uv_list[next].uv.y-sorted_uv_list[i].uv.y,sorted_uv_list[next].uv.x-sorted_uv_list[i].uv.x)) 416 | #check failcase of zero length vector: 417 | if vector1.length == 0 or vector2.length == 0: 418 | bmesh.update_edit_mesh(obj.data) 419 | print("zero vector") 420 | return False 421 | angle = -math.degrees(vector1.angle_signed(vector2)) 422 | if angle < 0: 423 | angle += 360 424 | sorted_angle_list.append(angle) 425 | 426 | 427 | #find concaves: 428 | for i in range(len(sorted_angle_list)): 429 | #print(sorted_angle_list[i]) 430 | if sorted_angle_list[i] > 230: 431 | distorted = True 432 | print("distorted!") 433 | bmesh.update_edit_mesh(obj.data) 434 | return False 435 | 436 | #angle test: 437 | #print("angles") 438 | #test if more than 4 90 degrees: 439 | NCount = 0 440 | for i in range(len(sorted_angle_list)): 441 | #print(sorted_angle_list[i]) 442 | if sorted_angle_list[i] < 100: 443 | NCount += 1 444 | if NCount > 4: 445 | distorted = True 446 | print("distorted2!") 447 | 448 | #now find top 4 angles 449 | topangles = list() 450 | for o in range(4): 451 | top = 360 452 | topindex = -1 453 | for i in range(len(sorted_angle_list)): 454 | if sorted_angle_list[i] < top: 455 | top = sorted_angle_list[i] 456 | topindex = i 457 | #print(sorted_angle_list[topindex]) 458 | 459 | if o == 3: 460 | if sorted_angle_list[topindex] > 120: 461 | distorted = True 462 | print("distorted3!") 463 | 464 | topangles.append(topindex) 465 | sorted_angle_list[topindex] = 999 #lol 466 | 467 | sorted_corner_list = list() 468 | for i in range(len(sorted_uv_list)): 469 | sorted_corner_list.append(False) 470 | sorted_corner_list[topangles[0]] = True 471 | sorted_corner_list[topangles[1]] = True 472 | sorted_corner_list[topangles[2]] = True 473 | sorted_corner_list[topangles[3]] = True 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | #find bottom left corner (using distance method seems to work well) 482 | distance = 2 483 | closest = 0 484 | for t in topangles: 485 | l = sorted_uv_list[t].uv.length 486 | if l < distance: 487 | distance = l 488 | closest = t 489 | 490 | #rotate lists to get clostest corner at start: 491 | for i in range(closest): 492 | sorted_corner_list.append(sorted_corner_list.pop(0)) 493 | sorted_uv_list.append(sorted_uv_list.pop(0)) 494 | sorted_vert_list.append(sorted_vert_list.pop(0)) 495 | 496 | #print("THESE ARE THE CORNERS") 497 | #print(sorted_corner_list) 498 | #create coord list: 499 | 500 | cornerz = list() 501 | 502 | for i in range(len(sorted_vert_list)): 503 | if sorted_corner_list[i] is True: 504 | #print(sorted_vert_list[i].co.z) 505 | cornerz.append(sorted_vert_list[i].co.z) 506 | 507 | #print(cornerz) 508 | #avgedge1 = cornerz[0] + cornerz[1] 509 | #avgedge2 = cornerz[0] + cornerz[3] 510 | #print(avgedge1) 511 | #print(avgedge2) 512 | 513 | 514 | 515 | 516 | sorted_edge_ratios = list() 517 | 518 | #get edge lenghts 519 | edge = list() 520 | for i in range(len(sorted_vert_list)): 521 | if sorted_corner_list[i] is True: 522 | sorted_edge_ratios.append(0) 523 | if i != 0: 524 | l = (sorted_vert_list[i-1].co.xyz - sorted_vert_list[i].co.xyz).length 525 | edge.append(sorted_edge_ratios[i-1] + l) 526 | 527 | if sorted_corner_list[i] is False: 528 | l = (sorted_vert_list[i-1].co.xyz - sorted_vert_list[i].co.xyz).length 529 | sorted_edge_ratios.append(sorted_edge_ratios[i-1] + l) 530 | if i is (len(sorted_vert_list)-1): 531 | l = (sorted_vert_list[i].co.xyz - sorted_vert_list[0].co.xyz).length 532 | edge.append(sorted_edge_ratios[i] + l) 533 | 534 | 535 | if quadmethod: 536 | print("running quadmethod") 537 | #MAP FIRST QUAD 538 | edge1 = (faces[0].loops[0].vert.co.xyz - faces[0].loops[1].vert.co.xyz).length 539 | edge2 = (faces[0].loops[1].vert.co.xyz - faces[0].loops[2].vert.co.xyz).length 540 | 541 | faces[0].loops[0][uv_layer].uv.x = 0 542 | faces[0].loops[0][uv_layer].uv.y = 0 543 | faces[0].loops[1][uv_layer].uv.x = edge1 544 | faces[0].loops[1][uv_layer].uv.y = 0 545 | faces[0].loops[2][uv_layer].uv.x = edge1 546 | faces[0].loops[2][uv_layer].uv.y = edge2 547 | faces[0].loops[3][uv_layer].uv.x = 0 548 | faces[0].loops[3][uv_layer].uv.y = edge2 549 | 550 | bm.faces.active = faces[0] 551 | 552 | #UNWRAP ADJACENT 553 | bmesh.update_edit_mesh(obj.data) 554 | bpy.ops.uv.follow_active_quads() 555 | obj = bpy.context.view_layer.objects.active 556 | bm = bmesh.from_edit_mesh(obj.data) 557 | uv_layer = bm.loops.layers.uv.verify() 558 | 559 | #return 560 | edge1 = (edge[0]+edge[2])*.5 561 | edge2 = (edge[1]+edge[3])*.5 562 | 563 | #FIT TO 0-1 range 564 | xmin, xmax = faces[0].loops[0][uv_layer].uv.x, faces[0].loops[0][uv_layer].uv.x 565 | ymin, ymax = faces[0].loops[0][uv_layer].uv.y, faces[0].loops[0][uv_layer].uv.y 566 | 567 | for face in faces: 568 | for vert in face.loops: 569 | xmin = min(xmin, vert[uv_layer].uv.x) 570 | xmax = max(xmax, vert[uv_layer].uv.x) 571 | ymin = min(ymin, vert[uv_layer].uv.y) 572 | ymax = max(ymax, vert[uv_layer].uv.y) 573 | 574 | #return 575 | 576 | #prevent divide by 0: 577 | if (xmax - xmin) == 0: 578 | xmin = .1 579 | if (ymax - ymin) == 0: 580 | ymin = .1 581 | 582 | for face in faces: 583 | for loop in face.loops: 584 | loop[uv_layer].uv.x -= xmin 585 | loop[uv_layer].uv.y -= ymin 586 | loop[uv_layer].uv.x /= (xmax-xmin) 587 | loop[uv_layer].uv.y /= (ymax-ymin) 588 | 589 | #shift extents to be positive only: 590 | xmax = xmax - xmin 591 | ymax = ymax - ymin 592 | 593 | #now fit to correct edge lengths: 594 | if (edge1 < edge2): 595 | #flip them: 596 | tedge = edge1 597 | edge1 = edge2 598 | edge2 = tedge 599 | 600 | if xmax >= ymax: 601 | for face in faces: 602 | for loop in face.loops: 603 | loop[uv_layer].uv.x *= edge1 604 | loop[uv_layer].uv.y *= edge2 605 | if xmax < ymax: 606 | for face in faces: 607 | for loop in face.loops: 608 | loop[uv_layer].uv.x *= edge2 609 | loop[uv_layer].uv.y *= edge1 610 | 611 | bmesh.update_edit_mesh(obj.data) 612 | 613 | 614 | if quadmethod is False: 615 | print("not running quadmethod") 616 | 617 | 618 | if distorted is False: 619 | #NOW LAY OUT ALL EDGE UVs 620 | i = 0 621 | #EDGE 1 622 | for l in sorted_vert_list[i].link_loops: 623 | if l.face.select is True: 624 | l[uv_layer].uv = Vector((0,0)) 625 | i += 1 626 | while sorted_corner_list[i] is False: 627 | for l in sorted_vert_list[i].link_loops: 628 | if l.face.select is True: 629 | l[uv_layer].uv = Vector((sorted_edge_ratios[i]/edge[0],0)) 630 | i += 1 631 | #EDGE 2 632 | for l in sorted_vert_list[i].link_loops: 633 | if l.face.select is True: 634 | l[uv_layer].uv = Vector((1,0)) 635 | i += 1 636 | while sorted_corner_list[i] is False: 637 | for l in sorted_vert_list[i].link_loops: 638 | if l.face.select is True: 639 | l[uv_layer].uv = Vector((1,sorted_edge_ratios[i]/edge[1])) 640 | i += 1 641 | #EDGE 3 642 | for l in sorted_vert_list[i].link_loops: 643 | if l.face.select is True: 644 | l[uv_layer].uv = Vector((1,1)) 645 | i += 1 646 | while sorted_corner_list[i] is False: 647 | for l in sorted_vert_list[i].link_loops: 648 | if l.face.select is True: 649 | l[uv_layer].uv = Vector((1-(sorted_edge_ratios[i]/edge[2]),1)) 650 | i += 1 651 | #EDGE 4 652 | for l in sorted_vert_list[i].link_loops: 653 | if l.face.select is True: 654 | l[uv_layer].uv = Vector((0,1)) 655 | i += 1 656 | for o in range(i,len(sorted_vert_list)): 657 | for l in sorted_vert_list[o].link_loops: 658 | if l.face.select is True: 659 | l[uv_layer].uv = Vector((0,1-(sorted_edge_ratios[o]/edge[3]))) 660 | 661 | #set proper aspect ratio 662 | 663 | for f in bm.faces: 664 | if f.select is True: 665 | for loop in f.loops: 666 | loop_uv = loop[uv_layer] 667 | loop_uv.uv.x *= edge[0] 668 | loop_uv.uv.y *= edge[1] 669 | 670 | bmesh.update_edit_mesh(me, True) 671 | bpy.ops.uv.select_all(action='SELECT') 672 | #expand middle verts 673 | bpy.ops.uv.minimize_stretch(iterations=100) 674 | #return true if rect fit was succesful 675 | return not distorted 676 | 677 | 678 | class subrect: 679 | aspect = int() 680 | posaspect = int() 681 | size = float() 682 | uvcoord = list() 683 | 684 | class trim: 685 | uvcoord = list() 686 | 687 | 688 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DreamUV 2 | 3 | ## About 4 | DreamUV is a collection of tools that allow you to manipulate UVs in the 3D viewport. This toolset is designed to be used with reusable textures like tiling textures, trimsheets and texture atlases. Its intent is to allow you to texture your geometry without having to exit the 3D view, saving you time and improving flexibility. 5 | 6 | ![screenshot](http://www.brameulaers.net/blender/addons/github_images/dreamuv_header.jpg) 7 | 8 | ## Installation 9 | 10 | * download as a zip and in user Preferences/Add-ons, use "Install from File..." 11 | * DreamUV should now be visible in the add-ons tab, in the UV category. 12 | * once activated, you can find DreamUV in the toolbar in Edit mode. 13 | 14 | ## Using DreamUV 15 | 16 | After installing, you can find the DreamUV toolset in the toolbar of the 3D viewport window. It'll appear when in edit mode. 17 | 18 | ![screenshot](http://www.brameulaers.net/blender/addons/github_images/dreamuv_tools.jpg) 19 | 20 | ## Viewport UV Tools 21 | 22 | The Viewport UV Tools are a collection of tool to directly manipulate UV maps in the 3D Viewport. These tools are particularly useful when working with tiling or trim based materials. 23 | 24 | The **Move, Scale and Rotate** tools allow you to transform your uvs in realtime just like you would be transforming a mesh. 25 | Some keyboard and mouse combos can be used while the tool is used. They have been set up to work similarly to the default Blender transform tools: 26 | 27 | * X: constrain translate/scale to x-axis 28 | * Y: constrain translate/scale to y-axis 29 | * MIDDLE MOUSE: hold and drag along the x or y axis to constrain the tool to that axis. Release to lock the constraint. 30 | * SHIFT: precision mode 31 | * CTRL: stepped mode 32 | * CTRL+SHIFT: stepped mode with smaller intervals 33 | 34 | The **Move, Scale and Rotate** tools also have buttons to transform your UV by discrete steps, which can be set using the snap sizes. 35 | 36 | The **extend** Tool will unwrap selected faces and attach them to the active face. 37 | 38 | The **Stitch** Tool will stitch the uvs of two selected faces on their shared edge 39 | 40 | The **Cycle** Tool will rotate the selected face's uvs by 90 degrees and keep it confined within its boundaries. 41 | 42 | The **Mirror** Tools will mirror selected uvs while also keeping the uvs confined within its boundaries. 43 | 44 | The **Move to UV Edge** will move the entire uv to the 0-1 uv boundary. This is useful to align a face to a texture's edge. 45 | 46 | ## Unwrapping Tools: 47 | 48 | **Square Fit Unwrap** will try to unwrap your selection fitted to a square. It's useful to unwrap patches, and is particularly useful when unwrapping pipes. 49 | 50 | **Blender Unwrap** is the standard blender unwrap tool. 51 | 52 | (more unwrapping tools are planned, like planar and box mapping options. Watch this space!) 53 | 54 | ## UV Transfer Tool: 55 | 56 | The **UV Transfer Tool** allows you to essentially copy paste a uv to a different spot. The top row are coordinates that represent a uv clipboard. You can type them in manually or use the grab from selection to save a UV boundary. The transfer to selection button will map the saved uv to your selection. 57 | 58 | ## Hotspotting: 59 | 60 | Hotspotting is a technique to quickly assign uv's to UV islands by referencing a predefined uv atlas. The DreamUV hotspot tool will attempt its best to find an appropriate sized rectangle on the atlas and fit it to the mesh geometry. 61 | 62 | For the hotspotting tool to work correctly, an atlas needs to be created. This is simply a mesh consisting of a variety of different sized rectangles. For example: 63 | ![screenshot](http://www.brameulaers.net/blender/addons/github_images/dreamuv_atlas.jpg) 64 | This is just one example layout, any layout should work. 65 | 66 | Keep in mind to scale the atlas geometry to a similar scale you want to uv's to be applied to your final mesh. Also make sure that the atlas object scale is set to 1 to make sure the sizes are transferred correctly. 67 | 68 | To hotspot a mesh, simply select the faces you want to hotspot and click the hotspot button. Clicking multiple times will make the tool cycle through different variations and uv placement. The mesh will be split into multiple uv islands that are hotspotted individually, using hard edges and seams. Its highly recommended to place extra seams manually to guide the tool and try to divide up your geometry into rectangular patches. 69 | ![screenshot](http://www.brameulaers.net/blender/addons/github_images/dreamuv_hotspot.jpg) 70 | 71 | If you have any feedback you can just message me via twitter: @leukbaars 72 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #this script is dedicated to the public domain under CC0 (https://creativecommons.org/publicdomain/zero/1.0/) 2 | #do whatever you want with it! 3 | 4 | bl_info = { 5 | "name": "DreamUV", 6 | "category": "UV", 7 | "author": "Bram Eulaers", 8 | "description": "Edit selected faces'UVs directly inside the 3D Viewport. WIP. Check for updates @leukbaars", 9 | "blender": (2, 90, 0), 10 | "version": (0, 9) 11 | } 12 | 13 | import bpy 14 | from bpy.props import EnumProperty, BoolProperty, FloatProperty, IntProperty, PointerProperty 15 | from . import DUV_UVTranslate 16 | from . import DUV_UVRotate 17 | from . import DUV_UVScale 18 | from . import DUV_UVExtend 19 | from . import DUV_UVStitch 20 | from . import DUV_UVTransfer 21 | from . import DUV_UVCycle 22 | from . import DUV_UVCopy 23 | from . import DUV_UVMirror 24 | from . import DUV_UVMoveToEdge 25 | from . import DUV_Utils 26 | from . import DUV_HotSpot 27 | from . import DUV_UVProject 28 | from . import DUV_UVUnwrap 29 | from . import DUV_UVInset 30 | from . import DUV_UVTrim 31 | from . import DUV_ApplyMaterial 32 | from . import DUV_UVBoxmap 33 | 34 | import importlib 35 | if 'bpy' in locals(): 36 | importlib.reload(DUV_UVTranslate) 37 | importlib.reload(DUV_UVRotate) 38 | importlib.reload(DUV_UVScale) 39 | importlib.reload(DUV_UVExtend) 40 | importlib.reload(DUV_UVStitch) 41 | importlib.reload(DUV_UVTransfer) 42 | importlib.reload(DUV_UVCycle) 43 | importlib.reload(DUV_UVCopy) 44 | importlib.reload(DUV_UVMirror) 45 | importlib.reload(DUV_UVMoveToEdge) 46 | importlib.reload(DUV_Utils) 47 | importlib.reload(DUV_HotSpot) 48 | importlib.reload(DUV_UVProject) 49 | importlib.reload(DUV_UVUnwrap) 50 | importlib.reload(DUV_UVInset) 51 | importlib.reload(DUV_UVTrim) 52 | importlib.reload(DUV_ApplyMaterial) 53 | importlib.reload(DUV_UVBoxmap) 54 | 55 | class DUVUVToolsPreferences(bpy.types.AddonPreferences): 56 | bl_idname = __name__ 57 | 58 | pixel_snap : BoolProperty( 59 | name="UV Pixel Snap", 60 | description="Translate Pixel Snapping", 61 | default=False 62 | ) 63 | 64 | move_snap : FloatProperty( 65 | name="UV Move Snap", 66 | description="Translate Scale Subdivision Snap Size", 67 | default=0.25 68 | ) 69 | scale_snap : FloatProperty( 70 | name="UV Scale Snap", 71 | description="Scale Snap Size", 72 | default=2 73 | ) 74 | rotate_snap : FloatProperty( 75 | name="UV Rotate Snap", 76 | description="Rotate Angle Snap Size", 77 | default=45 78 | ) 79 | 80 | 81 | # This should get its own py file 82 | class DREAMUV_PT_uv(bpy.types.Panel): 83 | """DreamUV Tools Panel Test!""" 84 | bl_label = "DreamUV" 85 | bl_space_type = 'VIEW_3D' 86 | bl_region_type = 'UI' 87 | bl_category = 'DreamUV' 88 | 89 | #@classmethod 90 | #def poll(cls, context): 91 | # prefs = bpy.context.preferences.addons[__name__].preferences 92 | # return prefs.show_panel_tools 93 | 94 | def draw(self, context): 95 | addon_prefs = prefs() 96 | layout = self.layout 97 | box = layout.box() 98 | if bpy.context.object.mode != 'EDIT': 99 | box.enabled = False 100 | 101 | col = box.column(align=True) 102 | col.label(text="Viewport UV Tools:") 103 | row = col.row(align = True) 104 | row.operator("view3d.dreamuv_uvtranslate", text="Move", icon="UV_SYNC_SELECT") 105 | row = row.row(align = True) 106 | row.prop(addon_prefs, 'move_snap', text="") 107 | 108 | row = col.row(align = True) 109 | op = row.operator("view3d.dreamuv_uvtranslatestep", text=" ", icon="TRIA_UP") 110 | op.direction = "up" 111 | op = row.operator("view3d.dreamuv_uvtranslatestep", text=" ", icon="TRIA_DOWN") 112 | op.direction = "down" 113 | op = row.operator("view3d.dreamuv_uvtranslatestep", text=" ", icon="TRIA_LEFT") 114 | op.direction = "left" 115 | op = row.operator("view3d.dreamuv_uvtranslatestep", text=" ", icon="TRIA_RIGHT") 116 | op.direction = "right" 117 | col.separator() 118 | 119 | row = col.row(align = True) 120 | row.operator("view3d.dreamuv_uvscale", text="Scale", icon="FULLSCREEN_ENTER") 121 | row = row.row(align = True) 122 | row.prop(addon_prefs, 'scale_snap', text="") 123 | row = col.row(align = True) 124 | op = row.operator("view3d.dreamuv_uvscalestep", text=" ", icon="ADD") 125 | op.direction="+XY" 126 | op = row.operator("view3d.dreamuv_uvscalestep", text=" ", icon="REMOVE") 127 | op.direction="-XY" 128 | op = row.operator("view3d.dreamuv_uvscalestep", text="+X") 129 | op.direction = "+X" 130 | op = row.operator("view3d.dreamuv_uvscalestep", text="-X") 131 | op.direction = "-X" 132 | op = row.operator("view3d.dreamuv_uvscalestep", text="+Y") 133 | op.direction = "+Y" 134 | op = row.operator("view3d.dreamuv_uvscalestep", text="-Y") 135 | op.direction = "-Y" 136 | col.separator() 137 | 138 | row = col.row(align = True) 139 | row.operator("view3d.dreamuv_uvrotate", text="Rotate", icon="FILE_REFRESH") 140 | row = row.row(align = True) 141 | row.prop(addon_prefs, 'rotate_snap', text="") 142 | row = col.row(align = True) 143 | op = row.operator("view3d.dreamuv_uvrotatestep", text=" ", icon="LOOP_FORWARDS") 144 | op.direction="forward" 145 | op = row.operator("view3d.dreamuv_uvrotatestep", text=" ", icon="LOOP_BACK") 146 | op.direction="reverse" 147 | col.separator() 148 | col.operator("view3d.dreamuv_uvextend", text="Extend", icon="MOD_TRIANGULATE") 149 | col.operator("view3d.dreamuv_uvstitch", text="Stitch", icon="UV_EDGESEL") 150 | col.operator("view3d.dreamuv_uvcycle", text="Cycle", icon="FILE_REFRESH") 151 | row = col.row(align = True) 152 | op = row.operator("view3d.dreamuv_uvmirror", text="Mirror X", icon="MOD_MIRROR") 153 | op.direction = "x" 154 | op = row.operator("view3d.dreamuv_uvmirror", text="Mirror Y") 155 | op.direction = "y" 156 | 157 | col.separator() 158 | row = col.row(align = True) 159 | op = row.operator("view3d.dreamuv_uvinsetstep", text="Inset", icon="FULLSCREEN_EXIT") 160 | op.direction = "in" 161 | op = row.operator("view3d.dreamuv_uvinsetstep", text="Expand", icon="FULLSCREEN_ENTER") 162 | op.direction = "out" 163 | row.prop(context.scene, "uvinsetpixels", text="") 164 | row.prop(context.scene, "uvinsettexsize", text="") 165 | 166 | col.label(text="Move to UV Edge:") 167 | row = col.row(align = True) 168 | op = row.operator("view3d.dreamuv_uvmovetoedge", text=" ", icon="TRIA_UP_BAR") 169 | op.direction="up" 170 | op = row.operator("view3d.dreamuv_uvmovetoedge", text=" ", icon="TRIA_DOWN_BAR") 171 | op.direction="down" 172 | op = row.operator("view3d.dreamuv_uvmovetoedge", text=" ", icon="TRIA_LEFT_BAR") 173 | op.direction = "left" 174 | op = row.operator("view3d.dreamuv_uvmovetoedge", text=" ", icon="TRIA_RIGHT_BAR") 175 | op.direction = "right" 176 | 177 | box = layout.box() 178 | if bpy.context.object.mode != 'EDIT': 179 | box.enabled = False 180 | col = box.column(align=True) 181 | col.label(text="Unwrapping Tools:") 182 | col.operator("view3d.dreamuv_uvunwrapsquare", text="Square Fit Unwrap", icon="OUTLINER_OB_LATTICE") 183 | unwraptool=col.operator("uv.unwrap", text="Blender Unwrap", icon='UV') 184 | unwraptool.method='CONFORMAL' 185 | unwraptool.margin=0.001 186 | 187 | 188 | col.separator() 189 | box = layout.box() 190 | if bpy.context.object.mode != 'EDIT': 191 | box.enabled = False 192 | col = box.column(align=True) 193 | col.label(text="UV Transfer Tool:") 194 | row = col.row(align = True) 195 | row.prop(context.scene, "uvtransferxmin", text="") 196 | row.prop(context.scene, "uvtransferymin", text="") 197 | row.prop(context.scene, "uvtransferxmax", text="") 198 | row.prop(context.scene, "uvtransferymax", text="") 199 | 200 | col.operator("view3d.dreamuv_uvtransfergrab", text="Grab UV from selection", icon="FILE_TICK") 201 | row = col.row(align = True) 202 | row.operator("view3d.dreamuv_uvtransfer", text="Transfer to selection", icon="MOD_UVPROJECT") 203 | 204 | col.separator() 205 | box = layout.box() 206 | col = box.column(align=True) 207 | col.label(text="HotSpot Tool:") 208 | 209 | #row.label(text="Atlas Object:") 210 | #row.prop_search(context.scene, "subrect_atlas", context.scene, "objects", text="", icon="MOD_MULTIRES") 211 | #row = col.row(align = True) 212 | 213 | col.separator() 214 | 215 | radiobutton = ( 216 | context.scene.duv_hotspot_atlas1, 217 | context.scene.duv_hotspot_atlas1, 218 | context.scene.duv_hotspot_atlas2, 219 | context.scene.duv_hotspot_atlas3, 220 | context.scene.duv_hotspot_atlas4, 221 | context.scene.duv_hotspot_atlas5, 222 | context.scene.duv_hotspot_atlas6, 223 | context.scene.duv_hotspot_atlas7, 224 | context.scene.duv_hotspot_atlas8, 225 | ) 226 | 227 | listsize = 1 228 | col.prop(context.scene, "atlas_list_size", text="atlas count:") 229 | 230 | 231 | while listsize <= context.scene.atlas_list_size: 232 | row = col.row(align = True) 233 | #row.prop(context.scene, "duv_hotspot_atlas1", icon="IPO_SINE", text="") 234 | if radiobutton[listsize]: 235 | op = row.operator("view3d.dreamuv_pushhotspot", text="", icon="RADIOBUT_ON") 236 | if not radiobutton[listsize]: 237 | op = row.operator("view3d.dreamuv_pushhotspot", text="", icon="RADIOBUT_OFF") 238 | op.index = listsize 239 | row.prop_search(context.scene, "subrect_atlas"+str(listsize), context.scene, "objects", text="", icon="MOD_MULTIRES") 240 | row.prop_search(context.scene, "duv_hotspotmaterial"+str(listsize), bpy.data, "materials", text="") 241 | listsize += 1 242 | 243 | col.separator() 244 | row = col.row(align = True) 245 | row.label(text="Atlas Scale:") 246 | row.prop(context.scene, "duvhotspotscale", text="") 247 | row = col.row(align = True) 248 | #row.label(text="Hotspot material:") 249 | #row.prop_search(context.scene, "duv_hotspotmaterial", bpy.data, "materials", text="") 250 | 251 | 252 | 253 | row = col.row(align = True) 254 | row.prop(context.scene, "duv_hotspotuseinset", icon="FULLSCREEN_EXIT", text="inset") 255 | row.separator() 256 | row.prop(context.scene, "hotspotinsetpixels", text="") 257 | row.prop(context.scene, "hotspotinsettexsize", text="") 258 | 259 | col.separator() 260 | row = col.row(align = True) 261 | row.operator("view3d.dreamuv_hotspotter", text="HotSpot", icon="SHADERFX") 262 | row.prop(context.scene, "duv_useorientation", icon="EVENT_W", text="") 263 | row.prop(context.scene, "duv_usemirrorx", icon="EVENT_X", text="") 264 | row.prop(context.scene, "duv_usemirrory", icon="EVENT_Y", text="") 265 | row.prop(context.scene, "duv_autoboxmap", icon="EVENT_B", text="") 266 | row.prop(context.scene, "duv_hotspot_uv1", icon="IPO_SINE", text="") 267 | row.prop(context.scene, "duv_hotspot_uv2", icon="IPO_QUAD", text="") 268 | 269 | #trim 270 | col.separator() 271 | box = layout.box() 272 | if bpy.context.object.mode != 'EDIT': 273 | box.enabled = False 274 | col = box.column(align=True) 275 | col.label(text="Trim Tool:") 276 | row = col.row(align = True) 277 | 278 | row.label(text="Trim/Cap Atlas:") 279 | row.prop_search(context.scene, "trim_atlas", context.scene, "objects", text="", icon="LINENUMBERS_ON") 280 | row = col.row(align = True) 281 | #row.prop(context.scene, "trim_index", text="") 282 | row.label(text="Trim index: "+str(context.scene.trim_index)) 283 | #row = col.row(align = True) 284 | op = row.operator("view3d.dreamuv_uvtrimnext", text=" ", icon="BACK") 285 | op.reverse = True 286 | op = row.operator("view3d.dreamuv_uvtrimnext", text=" ", icon="FORWARD") 287 | op.reverse = False 288 | 289 | row = col.row(align = True) 290 | row.label(text="Cap index: "+str(context.scene.cap_index)) 291 | #row = col.row(align = True) 292 | op = row.operator("view3d.dreamuv_uvcapnext", text=" ", icon="BACK") 293 | op.reverse = True 294 | op = row.operator("view3d.dreamuv_uvcapnext", text=" ", icon="FORWARD") 295 | op.reverse = False 296 | 297 | row = col.row(align = True) 298 | row.enabled = not context.scene.duv_uvtrim_randomshift 299 | row.prop(context.scene, "duv_uvtrim_bounds", icon="CENTER_ONLY", text="bounds") 300 | row.separator() 301 | row.prop(context.scene, "duv_uvtrim_min", text="") 302 | row.prop(context.scene, "duv_uvtrim_max", text="") 303 | 304 | col.separator() 305 | row = col.row(align = True) 306 | row.operator("view3d.dreamuv_uvtrim", text="Trim", icon="SEQ_SEQUENCER") 307 | 308 | row.operator("view3d.dreamuv_uvcap", text="Cap", icon="MOD_BUILD") 309 | 310 | row.prop(context.scene, "duv_uvtrim_randomshift", icon="NLA_PUSHDOWN", text="") 311 | row.prop(context.scene, "duv_autoboxmaptrim", icon="EVENT_B", text="") 312 | row.prop(context.scene, "duv_trimcap_uv1", icon="IPO_SINE", text="") 313 | row.prop(context.scene, "duv_trimcap_uv2", icon="IPO_QUAD", text="") 314 | 315 | 316 | 317 | #boxmap 318 | col.separator() 319 | box = layout.box() 320 | col = box.column(align=True) 321 | col.label(text="Boxmapping Tool:") 322 | row = col.row(align = True) 323 | row.label(text="Box Reference:") 324 | row.prop_search(context.scene, "uv_box", context.scene, "objects", text="", icon="MOD_MULTIRES") 325 | row = col.row(align = True) 326 | row.operator("view3d.dreamuv_uvboxmap", text="Boxmap", icon="FILE_3D") 327 | row.prop(context.scene, "duv_boxmap_uv1", icon="IPO_SINE", text="") 328 | row.prop(context.scene, "duv_boxmap_uv2", icon="IPO_QUAD", text="") 329 | 330 | 331 | col.separator() 332 | box = layout.box() 333 | col = box.column(align=True) 334 | col.label(text="UV sets:") 335 | row = col.row(align = True) 336 | 337 | op = row.operator("view3d.dreamuv_uvcopy", text="copy uv1->2", icon="XRAY") 338 | op.reverse = False 339 | op = row.operator("view3d.dreamuv_uvcopy", text="copy uv2->1", icon="XRAY") 340 | op.reverse = True 341 | 342 | if bpy.context.object.type == 'MESH': 343 | me = bpy.context.object.data 344 | #me = bpy.context.object.mesh 345 | 346 | row = col.row() 347 | col = row.column() 348 | 349 | col.template_list("MESH_UL_uvmaps", "uvmaps", me, "uv_layers", me.uv_layers, "active_index", rows=2) 350 | col = row.column(align=True) 351 | col.operator("mesh.uv_texture_add", icon='ADD', text="") 352 | col.operator("mesh.uv_texture_remove", icon='REMOVE', text="") 353 | 354 | #context.scene.duv_experimentaltools = True 355 | #if context.scene.duv_experimentaltools is True: 356 | col.separator() 357 | 358 | 359 | 360 | 361 | 362 | col = self.layout.column(align = True) 363 | col2= self.layout.column(align = True) 364 | col2.label(text="Send feedback to:") 365 | row = col2.row(align = True) 366 | row.label(text="@Leukbaars@mastodon.gamedev.place") 367 | row.prop(context.scene, "duv_experimentaltools", icon="HEART", text="") 368 | 369 | 370 | 371 | 372 | def prefs(): 373 | return bpy.context.preferences.addons[__name__].preferences 374 | 375 | classes = ( 376 | DUVUVToolsPreferences, 377 | DREAMUV_PT_uv, 378 | DUV_UVTranslate.DREAMUV_OT_uv_translate, 379 | DUV_UVTranslate.DREAMUV_OT_uv_translate_step, 380 | DUV_UVRotate.DREAMUV_OT_uv_rotate, 381 | DUV_UVRotate.DREAMUV_OT_uv_rotate_step, 382 | DUV_UVScale.DREAMUV_OT_uv_scale, 383 | DUV_UVScale.DREAMUV_OT_uv_scale_step, 384 | DUV_UVExtend.DREAMUV_OT_uv_extend, 385 | DUV_UVStitch.DREAMUV_OT_uv_stitch, 386 | DUV_UVTransfer.DREAMUV_OT_uv_transfer, 387 | DUV_UVTransfer.DREAMUV_OT_uv_transfer_grab, 388 | DUV_UVCycle.DREAMUV_OT_uv_cycle, 389 | DUV_UVCopy.DREAMUV_OT_uv_copy, 390 | DUV_UVMirror.DREAMUV_OT_uv_mirror, 391 | DUV_UVMoveToEdge.DREAMUV_OT_uv_move_to_edge, 392 | DUV_UVProject.DREAMUV_OT_uv_project, 393 | DUV_UVUnwrap.DREAMUV_OT_uv_unwrap_square, 394 | DUV_HotSpot.DREAMUV_OT_hotspotter, 395 | DUV_HotSpot.DREAMUV_OT_pushhotspot, 396 | DUV_UVInset.DREAMUV_OT_uv_inset, 397 | DUV_UVInset.DREAMUV_OT_uv_inset_step, 398 | DUV_UVTrim.DREAMUV_OT_uv_trim, 399 | DUV_UVTrim.DREAMUV_OT_uv_cap, 400 | DUV_UVTrim.DREAMUV_OT_uv_trimnext, 401 | DUV_UVTrim.DREAMUV_OT_uv_capnext, 402 | DUV_ApplyMaterial.DREAMUV_OT_apply_material, 403 | DUV_UVBoxmap.DREAMUV_OT_uv_boxmap, 404 | ) 405 | 406 | def poll_material(self, material): 407 | return not material.is_grease_pencil 408 | 409 | def register(): 410 | for cls in classes: 411 | bpy.utils.register_class(cls) 412 | 413 | bpy.types.Scene.subrect_atlas = bpy.props.PointerProperty (name="atlas",type=bpy.types.Object,description="atlas object") 414 | bpy.types.Scene.uv_box = bpy.props.PointerProperty (name="uvbox",type=bpy.types.Object,description="uv box") 415 | bpy.types.Scene.trim_atlas = bpy.props.PointerProperty (name="trim_atlas",type=bpy.types.Object,description="trim atlas") 416 | bpy.types.Scene.trim_index = bpy.props.IntProperty (name = "trim_index",default = 0,description = "trim index") 417 | bpy.types.Scene.cap_index = bpy.props.IntProperty (name = "cap_index",default = 0,description = "cap index") 418 | bpy.types.Scene.duv_trimuseinset = bpy.props.BoolProperty (name = "duv_trimuseinset",default = False,description = "Use inset when trimming") 419 | bpy.types.Scene.uvinsetpixels = bpy.props.FloatProperty (name = "uv inset pixel amount",default = 1.0,description = "") 420 | bpy.types.Scene.uvinsettexsize = bpy.props.FloatProperty (name = "uv inset texture size",default = 1024.0,description = "") 421 | bpy.types.Scene.uvtransferxmin = bpy.props.FloatProperty (name = "uvtransferxmin",default = 0.0,description = "uv left bottom corner X") 422 | bpy.types.Scene.uvtransferymin = bpy.props.FloatProperty (name = "uvtransferymin",default = 0.0,description = "uv left bottom corner Y") 423 | bpy.types.Scene.uvtransferxmax = bpy.props.FloatProperty (name = "uvtransferxmax",default = 1.0,description = "uv right top corner X") 424 | bpy.types.Scene.uvtransferymax = bpy.props.FloatProperty (name = "uvtransferymax",default = 1.0,description = "uv right top corner Y") 425 | bpy.types.Scene.duv_useorientation = bpy.props.BoolProperty (name = "duv_useorientation",default = False,description = "Align UVs with world orientation") 426 | bpy.types.Scene.duv_usemirrorx = bpy.props.BoolProperty (name = "duv_usemirrorx",default = True,description = "Randomly mirror faces on the x-axis") 427 | bpy.types.Scene.duv_usemirrory = bpy.props.BoolProperty (name = "duv_usemirrory",default = True,description = "Randomly mirror faces on the y-axis") 428 | bpy.types.Scene.duvhotspotscale = bpy.props.FloatProperty (name = "duvhotspotscale",default = 1.0,description = "Hotspotting scale multiplier") 429 | bpy.types.Scene.duv_hotspotmaterial = bpy.props.PointerProperty (name="duv_hotspotmaterial",type=bpy.types.Material,poll=poll_material,description="Hotspot material") 430 | bpy.types.Scene.duv_hotspotuseinset = bpy.props.BoolProperty (name = "duv_hotspotuseinset",default = False,description = "Use inset when hotspotting") 431 | bpy.types.Scene.hotspotinsetpixels = bpy.props.FloatProperty (name = "hotspot inset pixel amount",default = 1.0,description = "") 432 | bpy.types.Scene.hotspotinsettexsize = bpy.props.FloatProperty (name = "hotspot texture size",default = 1024.0,description = "") 433 | bpy.types.Scene.duv_experimentaltools = bpy.props.BoolProperty (name = "duv_experimentaltools",default = False,description = "Show experimental tools") 434 | bpy.types.Scene.duv_uv2copy = bpy.props.BoolProperty (name = "duv_uv2copy",default = False,description = "Copy uv2") 435 | bpy.types.Scene.duv_hotspot_uv1 = bpy.props.BoolProperty (name = "duv_hotspot_uv1",default = False,description = "Always hotspot UV 1") 436 | bpy.types.Scene.duv_hotspot_uv2 = bpy.props.BoolProperty (name = "duv_hotspot_uv2",default = False,description = "Always hotspot UV 2") 437 | bpy.types.Scene.duv_boxmap_uv1 = bpy.props.BoolProperty (name = "duv_boxmap_uv1",default = False,description = "Always box map UV 1") 438 | bpy.types.Scene.duv_boxmap_uv2 = bpy.props.BoolProperty (name = "duv_boxmap_uv2",default = False,description = "Always box map UV 2") 439 | bpy.types.Scene.duv_autoboxmap = bpy.props.BoolProperty (name = "duv_autoboxmap",default = False,description = "Auto apply boxmap after hotspot operation") 440 | bpy.types.Scene.duv_trimcap_uv1 = bpy.props.BoolProperty (name = "duv_trimcap_uv1",default = False,description = "Always apply trim/cap to UV 1") 441 | bpy.types.Scene.duv_trimcap_uv2 = bpy.props.BoolProperty (name = "duv_trimcap_uv2",default = False,description = "Always apply trim/cap to UV 2") 442 | bpy.types.Scene.duv_autoboxmaptrim = bpy.props.BoolProperty (name = "duv_autoboxmaptrim",default = False,description = "Auto apply boxmap after trim/cap operation") 443 | bpy.types.Scene.duv_uvtrim_randomshift = bpy.props.BoolProperty (name = "duv_uvtrim_randomshift",default = False,description = "Randomize trim position along tiling axis") 444 | bpy.types.Scene.duv_uvtrim_bounds = bpy.props.BoolProperty (name = "duv_uvtrim_bounds",default = False,description = "Scale trim to boundary region") 445 | bpy.types.Scene.duv_uvtrim_min = bpy.props.FloatProperty (name = "duv_uvtrim_min",default = 0.0,description = "Boundary start") 446 | bpy.types.Scene.duv_uvtrim_max = bpy.props.FloatProperty (name = "duv_uvtrim_max",default = 1.0,description = "Boundary end") 447 | 448 | bpy.types.Scene.subrect_atlas1 = bpy.props.PointerProperty (name="atlas1",type=bpy.types.Object,description="atlas1") 449 | bpy.types.Scene.subrect_atlas2 = bpy.props.PointerProperty (name="atlas2",type=bpy.types.Object,description="atlas2") 450 | bpy.types.Scene.subrect_atlas3 = bpy.props.PointerProperty (name="atlas3",type=bpy.types.Object,description="atlas3") 451 | bpy.types.Scene.subrect_atlas4 = bpy.props.PointerProperty (name="atlas4",type=bpy.types.Object,description="atlas4") 452 | bpy.types.Scene.subrect_atlas5 = bpy.props.PointerProperty (name="atlas5",type=bpy.types.Object,description="atlas5") 453 | bpy.types.Scene.subrect_atlas6 = bpy.props.PointerProperty (name="atlas6",type=bpy.types.Object,description="atlas6") 454 | bpy.types.Scene.subrect_atlas7 = bpy.props.PointerProperty (name="atlas7",type=bpy.types.Object,description="atlas7") 455 | bpy.types.Scene.subrect_atlas8 = bpy.props.PointerProperty (name="atlas8",type=bpy.types.Object,description="atlas8") 456 | 457 | bpy.types.Scene.duv_hotspotmaterial1 = bpy.props.PointerProperty (name="duv_hotspotmaterial1",type=bpy.types.Material,poll=poll_material,description="Hotspot material 1") 458 | bpy.types.Scene.duv_hotspotmaterial2 = bpy.props.PointerProperty (name="duv_hotspotmaterial2",type=bpy.types.Material,poll=poll_material,description="Hotspot material 2") 459 | bpy.types.Scene.duv_hotspotmaterial3 = bpy.props.PointerProperty (name="duv_hotspotmaterial3",type=bpy.types.Material,poll=poll_material,description="Hotspot material 3") 460 | bpy.types.Scene.duv_hotspotmaterial4 = bpy.props.PointerProperty (name="duv_hotspotmaterial4",type=bpy.types.Material,poll=poll_material,description="Hotspot material 4") 461 | bpy.types.Scene.duv_hotspotmaterial5 = bpy.props.PointerProperty (name="duv_hotspotmaterial5",type=bpy.types.Material,poll=poll_material,description="Hotspot material 5") 462 | bpy.types.Scene.duv_hotspotmaterial6 = bpy.props.PointerProperty (name="duv_hotspotmaterial6",type=bpy.types.Material,poll=poll_material,description="Hotspot material 6") 463 | bpy.types.Scene.duv_hotspotmaterial7 = bpy.props.PointerProperty (name="duv_hotspotmaterial7",type=bpy.types.Material,poll=poll_material,description="Hotspot material 7") 464 | bpy.types.Scene.duv_hotspotmaterial8 = bpy.props.PointerProperty (name="duv_hotspotmaterial8",type=bpy.types.Material,poll=poll_material,description="Hotspot material 8") 465 | 466 | bpy.types.Scene.duv_hotspot_atlas1 = bpy.props.BoolProperty (name = "duv_hotspot_atlas1",default = True,description = "duv_hotspot_atlas1") 467 | bpy.types.Scene.duv_hotspot_atlas2 = bpy.props.BoolProperty (name = "duv_hotspot_atlas2",default = False,description = "duv_hotspot_atlas2") 468 | bpy.types.Scene.duv_hotspot_atlas3 = bpy.props.BoolProperty (name = "duv_hotspot_atlas3",default = False,description = "duv_hotspot_atlas3") 469 | bpy.types.Scene.duv_hotspot_atlas4 = bpy.props.BoolProperty (name = "duv_hotspot_atlas4",default = False,description = "duv_hotspot_atlas4") 470 | bpy.types.Scene.duv_hotspot_atlas5 = bpy.props.BoolProperty (name = "duv_hotspot_atlas5",default = False,description = "duv_hotspot_atlas5") 471 | bpy.types.Scene.duv_hotspot_atlas6 = bpy.props.BoolProperty (name = "duv_hotspot_atlas6",default = False,description = "duv_hotspot_atlas6") 472 | bpy.types.Scene.duv_hotspot_atlas7 = bpy.props.BoolProperty (name = "duv_hotspot_atlas7",default = False,description = "duv_hotspot_atlas7") 473 | bpy.types.Scene.duv_hotspot_atlas8 = bpy.props.BoolProperty (name = "duv_hotspot_atlas8",default = False,description = "duv_hotspot_atlas8") 474 | 475 | bpy.types.Scene.atlas_list_size = bpy.props.IntProperty ( 476 | name = "atlas_list_size", 477 | default = 1, 478 | min = 1, 479 | max = 8, 480 | description = "atlas_list_size", 481 | ) 482 | 483 | 484 | def unregister(): 485 | for cls in reversed(classes): 486 | bpy.utils.unregister_class(cls) 487 | 488 | if __name__ == "__main__": 489 | register() 490 | -------------------------------------------------------------------------------- /example_atlas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leukbaars/DreamUV/c893b8c160ca307f0e8591826c42806f7fad713f/example_atlas.png --------------------------------------------------------------------------------