Paste Code
Paste Blends
Paste Images
bl_info = {
'name': 'Texture Paint plus',
'author': 'Bart Crouch, scorpion81 (straight line fix)',
'version': (1, 28),
'blender': (2, 6, 9),
'location': 'View3D > Texture Paint mode',
'warning': '',
'description': 'Several improvements for Texture Paint mode',
'wiki_url': '',
'tracker_url': '',
'category': 'Paint'}


import bgl
import blf
import bpy
import mathutils
import os
import time
import copy
import math
from bpy_extras.io_utils import ImportHelper


##########################################
# #
# Functions #
# #
##########################################

# check if any of the images has to be resized
def consolidate_check_resize(image_map):
resized = False
for [image, [x_min, y_min, x_max, y_max]] in image_map:
width = x_max - x_min
height = y_max - y_min
if width != image.size[0] or height != image.size[1]:
resized = True
break

return(resized)


# create a new image that contains all other images
def consolidate_copy_images(width, height, alpha, image_map, filename, fileformat, restrict_name):
# get image name
old_format = bpy.context.scene.render.image_settings.file_format
bpy.context.scene.render.image_settings.file_format = fileformat
extension = bpy.context.scene.render.file_extension
bpy.context.scene.render.image_settings.file_format = old_format
if len(extension) > len(filename) and filename[-len(extension):].lower() == extension.lower():
filename = filename[:-len(extension)]
if restrict_name != "consolidate":
filename += "_" + restrict_name

width = int(width)
height = int(height)
composite_image = bpy.data.images.new(filename, width, height, alpha=alpha)
composite_image.filepath_raw = filename + extension
composite_image.file_format = fileformat

composite_buffer = ImageBuffer(composite_image)
for [image, [x_offset, y_offset, x_max, y_max]] in image_map:
buffer = ImageBuffer(image)
for x in range(image.size[0]):
for y in range(image.size[1]):
composite_buffer.set_pixel(x+x_offset, y+y_offset, buffer.get_pixel(x, y))
composite_buffer.update()

# save and reload
composite_image.save()
bpy.data.images.remove(composite_image)
composite_image = bpy.data.images.load(filename + extension)

return(composite_image)


# retrieve list of images and associated data
def consolidate_get_images(meshes, restrict_values, all_ts):
images = []
textures = []
texture_slots = []
uv_layers = []

def is_within_restriction(texture_slot):
if not restrict_values:
return True
for value in restrict_values:
if getattr(texture_slot, value, False):
return True
return False

for mesh in meshes:
# scan texture slots
ts_found = False
for mat in mesh.materials:
if not mat:
continue
for ts in [ts for ts in mat.texture_slots if ts and ts.texture.type=='IMAGE']:
if ts.texture_coords == 'UV':
ts_found = True
if not is_within_restriction(ts):
continue
if ts in all_ts:
# texture slot already done in different consolidated image
continue
else:
texture_slots.append(ts)
image = ts.texture.image
if image not in images:
images.append(image)
if ts.texture not in textures:
textures.append(ts.texture)
uv_layer = ts.uv_layer
if not uv_layer:
uv_layer = mesh.uv_textures.active
else:
uv_layer = mesh.uv_textures[uv_layer]
uv_layers.append([mesh, image, mat, ts, uv_layer])
if not ts_found:
# scan game property assignments
# not possible anymore since these settings have been moved
pass
# for tex in mesh.uv_textures:
# for face in tex.data:
# if face.use_image:
# if face.image not in images:
# images.append(face.image)
# uv_layers.append([mesh, face.image, False, False, tex])

return(images, textures, texture_slots, ts_found, uv_layers)


# list of meshes for Consolidate Images operator
def consolidate_get_input(input):
if input == 'active':
meshes = []
ob = bpy.context.active_object
if ob and ob.type == 'MESH':
mesh = ob.data
if len(mesh.uv_textures) > 0:
meshes.append(bpy.context.active_object.data)
else: # input == 'selected'
meshes = [ob.data for ob in bpy.context.selected_objects if ob.type=='MESH' and len(ob.data.uv_textures)>0]

return(meshes)


# create a new image that contains all other images
def consolidate_new_image(width, height, alpha, image_map, filename, fileformat, restrict_name):
# create temporary scene
temp_scene = bpy.data.scenes.new("temp_scene")
temp_world = bpy.data.worlds.new("temp_world")
temp_scene.world = temp_world

# setup the camera
cam = bpy.data.cameras.new("temp_cam_ob")
cam.type = 'ORTHO'
cam.ortho_scale = 1.0
obj_cam = bpy.data.objects.new("temp_cam", cam)
obj_cam.location = [0.5, 0.5, 1.0]
if width < height:
obj_cam.location[0] = (width/height) / 2
elif height < width:
obj_cam.location[1] = (height/width) / 2
temp_scene.objects.link(obj_cam)
temp_scene.camera = obj_cam
obj_cam.layers[0] = True

# render settings
temp_scene.render.use_raytrace = False
temp_scene.render.alpha_mode = 'STRAIGHT'
if alpha:
temp_scene.render.image_settings.color_mode = 'RGBA'
else:
temp_scene.render.image_settings.color_mode = 'RGB'
temp_scene.render.resolution_x = width
temp_scene.render.resolution_y = height
temp_scene.render.resolution_percentage = 100
temp_scene.render.pixel_aspect_x = 1.0
temp_scene.render.pixel_aspect_y = 1.0
temp_scene.render.image_settings.file_format = fileformat
extension = temp_scene.render.file_extension
if len(extension) > len(filename) and filename[-len(extension):].lower() == extension.lower():
filename = filename[:-len(extension)]
if restrict_name != "consolidate":
filename += "_" + restrict_name
temp_scene.render.filepath = filename
temp_scene.render.antialiasing_samples = '16'
temp_scene.render.pixel_filter_type = 'MITCHELL'
temp_scene.render.use_file_extension = True
temp_scene.render.use_overwrite = True

# materials
total_object = [0, 0]
for [img, [x0, y0, x1, y1]] in image_map:
if x0 > total_object[0]:
total_object[0] = x0
if y0 > total_object[1]:
total_object[1] = y0
if x1 > total_object[0]:
total_object[0] = x1
if y1 > total_object[1]:
total_object[1] = y1
xtotal, ytotal = total_object

temp_materials = [[bpy.data.materials.new("temp_mat"), x0, y0, x1, y1] for [img, [x0, y0, x1, y1]] in image_map]
temp_textures = [bpy.data.textures.new("temp_tex", 'IMAGE') for i in range(len(image_map))]
for i, [temp_mat, x0, y0, x1, y1] in enumerate(temp_materials):
temp_mat.use_shadeless = True
temp_mat.use_face_texture = True
temp_mat.use_face_texture_alpha = True
temp_mat.use_transparency = True
temp_mat.transparency_method = 'Z_TRANSPARENCY'
temp_mat.alpha = 1.0
temp_mat.texture_slots.add()
temp_mat.texture_slots[0].texture = temp_textures[i]
temp_mat.texture_slots[0].texture.image = image_map[i][0]
# texture mapping
xlength = x1 - x0
xzoom = xtotal/(xlength)
xoriginoffset = 0.5 - (0.5 / xzoom)
xtargetoffset = x0 / xtotal
xoffset = (xtargetoffset - xoriginoffset) * -xzoom
ylength = y1 - y0
yzoom = ytotal/(ylength)
yoriginoffset = 0.5 - (0.5 / yzoom)
ytargetoffset = y0 / ytotal
yoffset = (ytargetoffset - yoriginoffset) * -yzoom
temp_mat.texture_slots[0].offset = [xoffset, yoffset, 0]
temp_mat.texture_slots[0].scale = [xzoom, yzoom, 1]

# mesh
temp_mesh = bpy.data.meshes.new("temp_mesh")
for [temp_mat, x0, y0, x1, y1] in temp_materials:
temp_mesh.materials.append(temp_mat)

temp_obj = bpy.data.objects.new("temp_object", temp_mesh)
temp_scene.objects.link(temp_obj)
temp_obj.layers[0] = True

new_vertices = []
new_faces = []
i = 0
for [img, [x0, y0, x1, y1]] in image_map:
new_vertices.extend([x0, y0, 0, x1, y0, 0, x1, y1, 0, x0, y1, 0])
new_faces.extend([i, i+1, i+2, i+3])
i += 4
temp_mesh.vertices.add(i)
temp_mesh.faces.add(i//4)
temp_mesh.vertices.foreach_set('co', new_vertices)
temp_mesh.faces.foreach_set('vertices_raw', new_faces)
temp_mesh.faces.foreach_set("material_index", range(i//4))
temp_mesh.update(calc_edges=True)
max_size = max(width, height)
temp_obj.scale = [1/max_size, 1/max_size, 1]

# render composite image
data_context = {"blend_data": bpy.context.blend_data, "scene": temp_scene}
bpy.ops.render.render(data_context, write_still=True)

# cleaning
bpy.data.scenes.remove(temp_scene)
bpy.data.objects.remove(obj_cam)
bpy.data.cameras.remove(cam)
bpy.data.objects.remove(temp_obj)
bpy.data.meshes.remove(temp_mesh)
for [temp_mat, x0, y0, x1, y1] in temp_materials:
bpy.data.materials.remove(temp_mat)
for temp_tex in temp_textures:
bpy.data.textures.remove(temp_tex)

# load composite image into blender's database
composite_image = bpy.data.images.load(filename + extension)

return(composite_image)


# bin pack images
def consolidate_pack_images(images, image_width, image_height, auto_size):
image_sizes = [[image.size[0]*image.size[1], image.size[0], image.size[1], image.name, image] for image in images]
image_sizes.sort()
total_area = sum([size[0] for size in image_sizes])
container_zoom = 1.5
zoom_delta = .1
fit = 0 # 0 = no fit tried yet, 1 = fit found, 2 = container too small
searching = True
image_map = []
iteration = 0

while searching:
iteration += 1
if iteration % 5 == 0:
# increase the container increase, to speed things up
zoom_delta *= 2
temp_map = []
success = True
image_area = image_width * image_height
image_zoom = total_area / image_area
container_width = image_width * image_zoom * container_zoom
container_height = image_height * image_zoom * container_zoom
tree = PackTree([container_width, container_height])
for area, width, height, name, image in image_sizes:
uv = tree.insert([width, height])
if not uv:
success = False
if fit == 1:
searching = False
break
fit = 2
container_zoom += zoom_delta
break
temp_map.append([image, uv.area])
if success:
fit = 1
image_map = temp_map
container_zoom -= zoom_delta

width = 0
height = 0
for image, [x_min, y_min, x_max, y_max] in image_map:
if x_max > width:
width = x_max
if y_max > height:
height = y_max

zoom = max([(width / image_width), (height / image_height)])
if auto_size:
image_width *= zoom
image_height *= zoom
else:
image_map = [[image, [x_min/zoom, y_min/zoom, x_max/zoom, y_max/zoom]] for image, [x_min, y_min, x_max, y_max] in image_map]

return(image_map, image_width, image_height)


def consolidate_update_textures(textures, uv_layers, image_map, composite_image, restrict_name):
# create remapped UV layers
total_width, total_height = composite_image.size
for [mesh, image, mat, ts, uv_layer] in uv_layers:
layer_names = [layer.name for layer in mesh.uv_textures]
new_name = uv_layer.name[:min(8, len(uv_layer.name))]
if restrict_name == "consolidate":
new_name += "_remap"
else:
new_name += "_" + restrict_name
if new_name in layer_names:
# problem, as we need a unique name
unique = False
n = 1
while not unique:
if new_name + "." + str(n).rjust(3, "0") not in layer_names:
new_name += "." + str(n).rjust(3, "0")
unique = True
break
else:
n += 1
if n > 999:
# couldn't find unique name
unique = True
break
new_layer = mesh.uv_textures.new(new_name)
if not new_layer:
continue
for [old_image, coords] in image_map:
if image == old_image:
break
offset_x, offset_y, x_max, y_max = coords
new_width = x_max - offset_x
new_height = y_max - offset_y
offset_x /= total_width
offset_y /= total_height
zoom_x = new_width / total_width
zoom_y = new_height / total_height
for i, mface in enumerate(uv_layer.data):
# get texture face data
pin_uv = mface.pin_uv
select_uv = mface.select_uv
uv_raw = [uv_co for uv_co in mface.uv_raw]
# remap UVs
for index in range(0, 8, 2):
uv_raw[index] = (uv_raw[index] * zoom_x) + offset_x
for index in range(1, 8, 2):
uv_raw[index] = (uv_raw[index] * zoom_y) + offset_y
# set texture face data
new_layer.data[i].uv_raw = uv_raw
new_layer.data[i].image = composite_image
new_layer.data[i].pin_uv = pin_uv
new_layer.data[i].select_uv = select_uv
if ts:
ts.uv_layer = new_layer.name
#ts.scale[0] /= (image.size[0] / composite_image.size[0])
#ts.scale[1] /= (image.size[1] / composite_image.size[1])
else:
mesh.uv_textures.active_index = len(mesh.uv_textures) - 1
# replace images with composite_image
for tex in textures:
tex.image = composite_image


# draw in 3d-view
def draw_callback(self, context):
r, g, b = context.tool_settings.image_paint.brush.cursor_color_add
#x0, y0, x1, y1 = context.window_manager["straight_line"]
start = self.stroke[0]
end = self.stroke[-1]

x0 = start["mouse"][0]
y0 = start["mouse"][1]

x1 = end["mouse"][0]
y1 = end["mouse"][1]

# draw straight line
bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(r, g, b, 1.0)
bgl.glBegin(bgl.GL_LINE_STRIP)
bgl.glVertex2i(x0, y0)
bgl.glVertex2i(x1, y1)
bgl.glEnd()
# restore opengl defaults
bgl.glDisable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)


# return a list of all images that are being displayed in an editor
def get_images_in_editors(context):
images = []
for area in context.screen.areas:
if area.type != 'IMAGE_EDITOR':
continue
for space in area.spaces:
if space.type != 'IMAGE_EDITOR':
continue
if space.image:
images.append(space.image)
area.tag_redraw()

return(images)


# calculate for 3d-view
def sync_calc_callback(self, context, area, region):
mid_x = region.width/2.0
mid_y = region.height/2.0
width = region.width
height = region.height

region_3d = False
for space in area.spaces:
if space.type == 'VIEW_3D':
region_3d = space.region_3d
if not region_3d:
return

view_mat = region_3d.perspective_matrix
ob_mat = context.active_object.matrix_world
total_mat = view_mat * ob_mat
mesh = context.active_object.data

def transform_loc(loc):
vec = total_mat * loc
vec = mathutils.Vector([vec[0]/vec[3], vec[1]/vec[3], vec[2]/vec[3]])
x = int(mid_x + vec[0]*width/2.0)
y = int(mid_y + vec[1]*height/2.0)

return([x, y])

# vertices
locs = [mesh.vertices[v].co.to_4d() for v in self.overlay_vertices]
self.position_vertices = []
for loc in locs:
self.position_vertices.append(transform_loc(loc))

# edges
locs = [[mesh.vertices[mesh.edges[edge].vertices[0]].co.to_4d(),
mesh.vertices[mesh.edges[edge].vertices[1]].co.to_4d()] \
for edge in self.overlay_edges]
self.position_edges = []
for v1, v2 in locs:
self.position_edges.append(transform_loc(v1))
self.position_edges.append(transform_loc(v2))

# faces
locs = [[mesh.vertices[mesh.faces[face].vertices[0]].co.to_4d(),
mesh.vertices[mesh.faces[face].vertices[1]].co.to_4d(),
mesh.vertices[mesh.faces[face].vertices[2]].co.to_4d(),
mesh.vertices[mesh.faces[face].vertices[3]].co.to_4d(),] \
for face in self.overlay_faces]
self.position_faces = []
for v1, v2, v3, v4 in locs:
self.position_faces.append(transform_loc(v1))
self.position_faces.append(transform_loc(v2))
self.position_faces.append(transform_loc(v3))
self.position_faces.append(transform_loc(v4))


# draw in 3d-view
def sync_draw_callback(self, context):
# polling
if context.mode != "EDIT_MESH":
return

# draw vertices
bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
bgl.glPointSize(4)
bgl.glBegin(bgl.GL_POINTS)
for x, y in self.position_vertices:
bgl.glVertex2i(x, y)
bgl.glEnd()

# draw edges
bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
bgl.glLineWidth(1.5)
bgl.glBegin(bgl.GL_LINES)
for x, y in self.position_edges:
bgl.glVertex2i(x, y)
bgl.glEnd()
bgl.glLineWidth(1)

# draw faces
bgl.glEnable(bgl.GL_BLEND)
bgl.glColor4f(1.0, 0.0, 0.0, 0.3)
bgl.glBegin(bgl.GL_QUADS)
for x, y in self.position_faces:
bgl.glVertex2i(x, y)
bgl.glEnd()
bgl.glDisable(bgl.GL_BLEND)


# draw in image-editor
def sync_draw_callback2(self, context):
# polling
if context.mode != "EDIT_MESH":
return

# draw vertices
bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
bgl.glPointSize(6)
bgl.glBegin(bgl.GL_POINTS)
for x, y in self.position2_vertices:
bgl.glVertex2f(x, y)
bgl.glEnd()


# draw paint tool and blendmode in 3d-view
def toolmode_draw_callback(self, context):
# polling
if context.mode != 'PAINT_TEXTURE':
return

# draw
if context.region:
main_y = context.region.height - 32
else:
return
blend_dic = {"MIX": "Mix",
"ADD": "Add",
"SUB": "Subtract",
"MUL": "Multiply",
"LIGHTEN": "Lighten",
"DARKEN": "Darken",
"ERASE_ALPHA": "Erase Alpha",
"ADD_ALPHA": "Add Alpha"}
brush = context.tool_settings.image_paint.brush
text = brush.name + " - " + blend_dic[brush.blend]

# text in top-left corner
bgl.glColor3f(0.6, 0.6, 0.6)
blf.position(0, 21, main_y, 0)
blf.draw(0, text)

# text above brush
dt = time.time() - context.window_manager["tpp_toolmode_time"]
if dt < 1:
if "tpp_toolmode_brushloc" not in context.window_manager:
return
brush_x, brush_y = context.window_manager["tpp_toolmode_brushloc"]
brush_x -= blf.dimensions(0, text)[0] / 2
bgl.glColor4f(0.6, 0.6, 0.6, min(1.0, (1.0 - dt)*2))
blf.position(0, brush_x, brush_y, 0)
blf.draw(0, text)


# add ID-properties to window-manager
def init_props():
wm = bpy.context.window_manager
wm["tpp_automergeuv"] = 0


# remove ID-properties from window-manager
def remove_props():
wm = bpy.context.window_manager
if "tpp_automergeuv" in wm:
del wm["tpp_automergeuv"]
if "tpp_toolmode_time" in wm:
del wm["tpp_toolmode_time"]
if "tpp_toolmode_brushloc" in wm:
del wm["tpp_toolmode_brusloc"]


# calculate new snapped location based on start point (sx,sy)
# and current mouse point (mx,my). These coords appear to be
# in 2D screen coords, with the origin at bottom-left, +x right,
# +y up.
#
def do_snap( sx, sy, mx, my ):
# compute delta between current mouse position and
# start position
dx = mx - sx
dy = my - sy
adx = abs(dx)
ady = abs(dy)

# if delta is "close enough" to the diagonal
if abs( ady - adx ) < 0.5 * max(adx, ady):

# use a simple algorithm to snap based on horizontal
# distance (could use vertical distance, or could use
# radial distance but that would require more calcs).
if (dx > 0 and dy > 0) or (dx < 0 and dy < 0):
x = mx
y = sy + dx
elif (dx > 0 and dy < 0) or (dx < 0 and dy > 0):
x = mx
y = sy - dx
else:
x = mx
y = my
elif ( adx > ady ):
# closer to y-axis, snap vertical
x = mx
y = sy
else:
# closer to x-axis, snap horizontal
x = sx
y = my

return (x, y)




##########################################
# #
# Classes #
# #
##########################################

class ImageBuffer:
# based on script by Domino from BlenderArtists
# licensed GPL v2 or later
def __init__(self, image):
self.image = image
self.x, self.y = self.image.size
self.buffer = list(self.image.pixels)

def update(self):
self.image.pixels = self.buffer

def _index(self, x, y):
if x < 0 or y < 0 or x >= self.x or y >= self.y:
return None
return (x + y * self.x) * 4

def set_pixel(self, x, y, colour):
index = self._index(x, y)
if index is not None:
index = int(index)
self.buffer[index:index + 4] = colour

def get_pixel(self, x, y):
index = self._index(x, y)
if index is not None:
index = int(index)
return self.buffer[index:index + 4]
else:
return None


# 2d bin packing
class PackTree(object):
# based on python recipe by S W on ActiveState
# PSF license, 16 oct 2005. (GPL compatible)
def __init__(self, area):
if len(area) == 2:
area = (0,0,area[0],area[1])
self.area = area

def get_width(self):
return self.area[2] - self.area[0]
width = property(fget=get_width)

def get_height(self):
return self.area[3] - self.area[1]
height = property(fget=get_height)

def insert(self, area):
if hasattr(self, 'child'):
a = self.child[0].insert(area)
if a is None:
return self.child[1].insert(area)
else:
return a

area = PackTree(area)
if area.width <= self.width and area.height <= self.height:
self.child = [None,None]
self.child[0] = PackTree((self.area[0]+area.width, self.area[1], self.area[2], self.area[1] + area.height))
self.child[1] = PackTree((self.area[0], self.area[1]+area.height, self.area[2], self.area[3]))
return PackTree((self.area[0], self.area[1], self.area[0]+area.width, self.area[1]+area.height))


##########################################
# #
# Operators #
# #
##########################################

class AddDefaultImage(bpy.types.Operator):
'''Create and assign a new default image to the object'''
bl_idname = "object.add_default_image"
bl_label = "Add default image"

@classmethod
def poll(cls, context):
return(context.active_object and context.active_object.type=='MESH')

def invoke(self, context, event):
ob = context.active_object
mat = bpy.data.materials.new("default")
tex = bpy.data.textures.new("default", 'IMAGE')
img = bpy.data.images.new("default", 1024, 1024, alpha=True)
ts = mat.texture_slots.add()
tex.image = img
ts.texture = tex
ob.data.materials.append(mat)

return {'FINISHED'}


class AutoMergeUV(bpy.types.Operator):
'''Have UV Merge enabled by default for merge actions'''
bl_idname = "paint.auto_merge_uv"
bl_label = "AutoMerge UV"

def invoke(self, context, event):
wm = context.window_manager
if "tpp_automergeuv" not in wm:
init_props()
wm["tpp_automergeuv"] = 1 - wm["tpp_automergeuv"]

km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
for kmi in km.keymap_items:
if kmi.idname == "mesh.merge":
kmi.properties.uvs = wm["tpp_automergeuv"]

return {'FINISHED'}


class BrushPopup(bpy.types.Operator):
bl_idname = "paint.brush_popup"
bl_label = "Brush settings"
bl_options = {'REGISTER'}

def draw(self, context):
brush = context.tool_settings.image_paint.brush

# colour buttons
col = self.layout.column()
split = col.split(percentage = 0.15)
split.prop(brush, "color", text="")
split.scale_y = 1e-6
col.template_color_wheel(brush, "color", value_slider=True)

# imagepaint tool buttons
group = col.column(align=True)
row = group.row(align=True)
row.prop(brush, "image_tool", expand=True, icon_only=True, emboss=True)

# curve type buttons
row = group.split(align=True)
row.operator("brush.curve_preset", icon="SMOOTHCURVE", text="").shape = 'SMOOTH'
row.operator("brush.curve_preset", icon="SPHERECURVE", text="").shape = 'ROUND'
row.operator("brush.curve_preset", icon="ROOTCURVE", text="").shape = 'ROOT'
row.operator("brush.curve_preset", icon="SHARPCURVE", text="").shape = 'SHARP'
row.operator("brush.curve_preset", icon="LINCURVE", text="").shape = 'LINE'
row.operator("brush.curve_preset", icon="NOCURVE", text="").shape = 'MAX'

# radius buttons
col = col.column(align=True)
row = col.row(align=True)
row.prop(brush, "size", text="Radius", slider=True)
row.prop(brush, "use_pressure_size", toggle=True, text="")
# strength buttons
row = col.row(align=True)
row.prop(brush, "strength", text="Strength", slider=True)
row.prop(brush, "use_pressure_strength", toggle=True, text="")
# jitter buttons
row = col.row(align=True)
row.prop(brush, "jitter", slider=True)
row.prop(brush, "use_pressure_jitter", toggle=True, text="")
# spacing buttons
row = col.row(align=True)
row.prop(brush, "spacing", slider=True)
row.prop(brush, "use_space", toggle=True, text="", icon="FILE_TICK")

# alpha and blending mode buttons
split = col.split(percentage=0.25)
row = split.row()
row.active = (brush.blend not in {'ERASE_ALPHA', 'ADD_ALPHA'})
row.prop(brush, "use_alpha", text="A")
split.prop(brush, "blend", text="")

def execute(self, context):
if context.space_data.type == 'IMAGE_EDITOR':
context.space_data.mode = 'PAINT'
return context.window_manager.invoke_popup(self, width=125)


class ChangeSelection(bpy.types.Operator):
'''Select more or less vertices/edges/faces, connected to the original selection'''
bl_idname = "paint.change_selection"
bl_label = "Change selection"

mode = bpy.props.EnumProperty(name="Mode",
items = (("more", "More", "Select more vertices/edges/faces"),
("less", "Less", "Select less vertices/edges/faces")),
description = "Choose whether the selection should be increased or decreased",
default = 'more')

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def invoke(self, context, event):
bpy.ops.object.mode_set(mode='EDIT')
if self.mode == 'more':
bpy.ops.mesh.select_more()
else: #self.mode == 'less'
bpy.ops.mesh.select_less()
bpy.ops.object.mode_set(mode='TEXTURE_PAINT')

return {'FINISHED'}


class ConsolidateImages(bpy.types.Operator):
'''Pack all texture images into one image'''
bl_idname = "image.consolidate"
bl_label = "Consolidate images"

remap = bpy.props.BoolProperty(default=False)

@classmethod
def poll(cls, context):
return(context.active_object or context.selected_objects)

def invoke(self, context, event):
props = context.window_manager.tpp
meshes = consolidate_get_input(props.consolidate_input)
if not meshes:
self.report({'ERROR'}, "No UV textures found")
return {'CANCELLED'}

if props.consolidate_split:
restrict = {"diffuse":["use_map_diffuse", "use_map_color_diffuse", "use_map_alpha", "use_map_translucency"],
"shading":["use_map_ambient", "use_map_emit", "use_map_mirror", "use_map_raymir"],
"specular":["use_map_specular", "use_map_color_spec", "use_map_hardness"],
"geometry":["use_map_normal", "use_map_warp", "use_map_displacement"]}
else:
restrict = {"consolidate":False}

all_ts = []
for restrict_name, restrict_values in restrict.items():
if context.area:
context.area.header_text_set("Collecting images")
images, textures, texture_slots, ts_found, uv_layers = consolidate_get_images(meshes, restrict_values, all_ts)
if not images:
continue
all_ts += texture_slots
if not ts_found:
restrict_name = "consolidate"
restrict_values = False
if context.area:
context.area.header_text_set("Bin packing images")
image_map, width, height = consolidate_pack_images(images, props.consolidate_width, props.consolidate_height, props.consolidate_auto_size)
if context.area:
context.area.header_text_set("Rendering composite image")
resized = consolidate_check_resize(image_map)
if not resized:
# direct copy from source
composite_image = consolidate_copy_images(width, height, props.consolidate_alpha, image_map, props.consolidate_filename, props.consolidate_fileformat, restrict_name)
else:
# render to enable resizing
composite_image = consolidate_new_image(width, height, props.consolidate_alpha, image_map, props.consolidate_filename, props.consolidate_fileformat, restrict_name)
if self.remap:
if context.area:
context.area.header_text_set("Updating textures and UVs")
consolidate_update_textures(textures, uv_layers, image_map, composite_image, restrict_name)
if not ts_found:
break

if context.area:
context.area.header_text_set()
return {'FINISHED'}


class CycleBlendtype(bpy.types.Operator):
'''Change the transparency blending mode of the active texture face'''
bl_idname = "paint.cycle_blendtype"
bl_label = "Cycle transparency blending mode"

@classmethod
def poll(cls, context):
ob = context.active_object
if not ob or not ob.data:
return False
return ob.type == 'MESH'

def invoke(self, context, event):
object = context.active_object
mesh = object.data
old_mode = object.mode
bpy.ops.object.mode_set(mode="OBJECT")

if context.tool_settings.use_uv_select_sync:
# use MeshFace selection state
faces = [[tex, i] for tex in mesh.uv_textures for i, face in enumerate(tex.data) if mesh.faces[i].select]
else:
# get MeshTextureFace selection state
faces = [[tex, i] for tex in mesh.uv_textures for i, face in enumerate(tex.data) if [uv_sel for uv_sel in face.select_uv]==[True,True,True,True]]
if faces:
old_type = faces[0][0].data[faces[0][1]].blend_type
if old_type == 'OPAQUE':
new_type = 'ALPHA'
elif old_type == 'ALPHA':
new_type = 'CLIPALPHA'
else:
new_type = 'OPAQUE'
for [tex, i] in faces:
tex.data[i].blend_type = new_type
bpy.ops.object.mode_set(mode=old_mode)

return {'FINISHED'}


class DefaultMaterial(bpy.types.Operator):
'''Add a default dif/spec/normal material to an object'''
bl_idname = "object.default_material"
bl_label = "Default material"

@classmethod
def poll(cls, context):
object = context.active_object
if not object or not object.data:
return False
return object.type == 'MESH'

def invoke(self, context, event):
objects = context.selected_objects
for ob in objects:
if not ob.data or ob.type != 'MESH':
continue

mat = bpy.data.materials.new(ob.name)

# diffuse texture
tex = bpy.data.textures.new(ob.name+"_D", 'IMAGE')
ts = mat.texture_slots.add()
ts.texture_coords = 'UV'
ts.texture = tex
# specular texture
tex = bpy.data.textures.new(ob.name+"_S", 'IMAGE')
ts = mat.texture_slots.add()
ts.texture_coords = 'UV'
ts.use_map_color_diffuse = False
ts.use_map_specular = True
ts.texture = tex
# normal texture
tex = bpy.data.textures.new(ob.name+"_N", 'IMAGE')
tex.use_normal_map = True
ts = mat.texture_slots.add()
ts.texture_coords = 'UV'
ts.use_map_color_diffuse = False
ts.use_map_normal = True
ts.texture = tex

ob.data.materials.append(mat)

return {'FINISHED'}



class DisplayToolMode(bpy.types.Operator):
'''Display paint tool and blend mode in the 3d-view'''
bl_idname = "paint.display_tool_mode"
bl_label = "Tool + Mode"

_handle = None
_timer = None

@classmethod
def poll(cls, context):
return context.mode == 'PAINT_TEXTURE'

def modal(self, context, event):
if context.window_manager.tpp.toolmode_enabled == -1:
context.window_manager.event_timer_remove(self._timer)
context.region.callback_remove(self._handle)
context.window_manager.tpp.toolmode_enabled = 0
if context.area:
context.area.tag_redraw()
return {'CANCELLED'}

if context.area:
context.area.tag_redraw()

if event.type == 'TIMER':
brush = context.tool_settings.image_paint.brush
if brush.name != context.window_manager.tpp.toolmode_tool:
context.window_manager.tpp.toolmode_tool = brush.name
context.window_manager["tpp_toolmode_time"] = time.time()
if brush.blend != context.window_manager.tpp.toolmode_mode:
context.window_manager.tpp.toolmode_mode = brush.blend
context.window_manager["tpp_toolmode_time"] = time.time()
if time.time() - context.window_manager["tpp_toolmode_time"] < 1:
x = event.mouse_region_x
y = event.mouse_region_y + brush.size + 5
context.window_manager["tpp_toolmode_brushloc"] = (x, y)

return {'PASS_THROUGH'}

def cancel(self, context):
try:
context.window_manager.event_timer_remove(self._timer)
context.region.callback_remove(self._handle)
context.window_manager.tpp.toolmode_enabled = 0
except:
pass
return {'CANCELLED'}

def invoke(self, context, event):
if context.window_manager.tpp.toolmode_enabled == 0:
brush = context.tool_settings.image_paint.brush
context.window_manager.tpp.toolmode_enabled = 1
context.window_manager["tpp_toolmode_time"] = time.time()
context.window_manager.tpp.toolmode_tool = brush.name
context.window_manager.tpp.toolmode_mode = brush.blend
context.window_manager.modal_handler_add(self)
self._handle = context.region.callback_add(toolmode_draw_callback,
(self, context), 'POST_PIXEL')
self._timer = context.window_manager.event_timer_add(0.02, context.window)
else:
context.window_manager.tpp.toolmode_enabled = -1
return {'CANCELLED'}

return {'RUNNING_MODAL'}



class GridTexture(bpy.types.Operator):
'''Toggle between current texture and UV / Colour grids'''
bl_idname = "paint.grid_texture"
bl_label = "Grid texture"

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def invoke(self, context, event):
objects = bpy.context.selected_objects
meshes = [object.data for object in objects if object.type == 'MESH']
if not meshes:
self.report({'INFO'}, "Couldn't locate meshes to operate on")
return {'CANCELLED'}

tex_image = []
for mesh in meshes:
for mat in mesh.materials:
for tex in [ts.texture for ts in mat.texture_slots if ts and ts.texture.type=='IMAGE' and ts.texture.image]:
tex_image.append([tex.name, tex.image.name])
if not tex_image:
self.report({'INFO'}, "Couldn't locate textures to operate on")
return {'CANCELLED'}

first_image = bpy.data.images[tex_image[0][1]]
if "grid_texture_mode" in first_image:
mode = first_image["grid_texture_mode"]
else:
mode = 1

if mode == 1:
# original textures, change to new UV grid
width = max([bpy.data.images[image].size[0] for tex, image in tex_image])
height = max([bpy.data.images[image].size[1] for tex, image in tex_image])
new_image = bpy.data.images.new("temp_grid", width=width, height=height)
new_image.generated_type = 'UV_GRID'
new_image["grid_texture"] = tex_image
new_image["grid_texture_mode"] = 2
for tex, image in tex_image:
bpy.data.textures[tex].image = new_image
elif mode == 2:
# change from UV grid to Colour grid
first_image.generated_type = 'COLOR_GRID'
first_image["grid_texture_mode"] = 3
elif mode == 3:
# change from Colour grid back to original textures
if "grid_texture" not in first_image:
first_image["grid_texture_mode"] = 1
self.report({'ERROR'}, "Couldn't retrieve original images")
return {'FINISHED'}
tex_image = first_image["grid_texture"]
for tex, image in tex_image:
if tex in bpy.data.textures and image in bpy.data.images:
bpy.data.textures[tex].image = bpy.data.images[image]
bpy.data.images.remove(first_image)

return {'FINISHED'}


class MassLinkAppend(bpy.types.Operator, ImportHelper):
'''Import objects from multiple blend-files at the same time'''
bl_idname = "wm.mass_link_append"
bl_label = "Mass Link/Append"
bl_options = {'REGISTER', 'UNDO'}

active_layer = bpy.props.BoolProperty(name="Active Layer",
default=True,
description="Put the linked objects on the active layer")
autoselect = bpy.props.BoolProperty(name="Select",
default=True,
description="Select the linked objects")
instance_groups = bpy.props.BoolProperty(name="Instance Groups",
default=False,
description="Create instances for each group as a DupliGroup")
link = bpy.props.BoolProperty(name="Link",
default=False,
description="Link the objects or datablocks rather than appending")
relative_path = bpy.props.BoolProperty(name="Relative Path",
default=True,
description="Select the file relative to the blend file")

def execute(self, context):
directory, filename = os.path.split(bpy.path.abspath(self.filepath))
files = []

# find all blend-files in the given directory
for root, dirs, filenames in os.walk(directory):
for file in filenames:
if file.endswith(".blend"):
files.append([root+os.sep, file])
break # don't search in subdirectories

# append / link objects
old_selection = context.selected_objects
new_selection = []
print("_______ Texture Paint Plus _______")
print("You can safely ignore the line(s) below")
for directory, filename in files:
# get object names
with bpy.data.libraries.load(directory + filename) as (append_lib, current_lib):
ob_names = append_lib.objects
for name in ob_names:
append_libs = [{"name":name} for name in ob_names]
# appending / linking
bpy.ops.wm.link_append(filepath=os.sep+filename+os.sep+"Object"+os.sep,
filename=name, directory=directory+filename+os.sep+"Object"+os.sep,
link=self.link, autoselect=True, active_layer=self.active_layer,
relative_path=self.relative_path, instance_groups=self.instance_groups,
files=append_libs)
if not self.link:
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.make_local()
bpy.ops.object.make_local(type='SELECTED_OBJECTS_DATA')
new_selection += context.selected_objects
print("__________________________________")
bpy.ops.object.select_all(action='DESELECT')
if self.autoselect:
for ob in new_selection:
ob.select = True
else:
for ob in old_selection:
ob.select = True

return {'FINISHED'}


class OriginSet(bpy.types.Operator):
'''Set origin while in editmode'''
bl_idname = "mesh.origin_set"
bl_label = "Set Origin"

type = bpy.props.EnumProperty(name="Type",
items = (("GEOMETRY_ORIGIN", "Geometry to Origin", "Move object geometry to object origin"),
("ORIGIN_GEOMETRY", "Origin to Geometry", "Move object origin to center of object geometry"),
("ORIGIN_CURSOR", "Origin to 3D Cursor", "Move object origin to position of the 3d cursor")),
default = 'GEOMETRY_ORIGIN')
center = bpy.props.EnumProperty(name="Center",
items=(("MEDIAN", "Median Center", ""),
("BOUNDS", "Bounds Center", "")),
default = 'MEDIAN')

@classmethod
def poll(cls, context):
ob = context.active_object
if not ob or not ob.data:
return False
return ob.type == 'MESH'

def execute(self, context):
object = context.active_object
mesh = object.data
old_mode = object.mode
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.origin_set(type=self.type, center=self.center)
bpy.ops.object.mode_set(mode=old_mode)

return {'FINISHED'}


class ReloadImage(bpy.types.Operator):
'''Reload image displayed in image-editor'''
bl_idname = "paint.reload_image"
bl_label = "Reload image"

def invoke(self, context, event):
images = get_images_in_editors(context)
for img in images:
img.reload()

# make the changes immediately visible in 3d-views
# image editor updating is handled in get_images_in_editors()
for area in context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()

return{'FINISHED'}


class ReloadImages(bpy.types.Operator):
'''Reload all images'''
bl_idname = "paint.reload_images"
bl_label = "Reload all images"

def invoke(self, context, event):
reloaded = [0, 0]
for img in bpy.data.images:
img.reload()

# make the changes immediately visible in image editors and 3d-views
for area in context.screen.areas:
if area.type == 'IMAGE_EDITOR' or area.type == 'VIEW_3D':
area.tag_redraw()

return {'FINISHED'}


class SampleColor(bpy.types.Operator):
'''Sample color'''
bl_idname = "paint.sample_color_custom"
bl_label = "Sample color"

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def invoke(self, context, event):
mesh = context.active_object.data
paint_mask = mesh.use_paint_mask
mesh.use_paint_mask = False
bpy.ops.paint.sample_color('INVOKE_REGION_WIN')
mesh.use_paint_mask = paint_mask

return {'FINISHED'}


class SaveImage(bpy.types.Operator):
'''Save image displayed in image-editor'''
bl_idname = "paint.save_image"
bl_label = "Save image"

def invoke(self, context, event):
images = get_images_in_editors(context)
for img in images:
img.save()

return{'FINISHED'}


class SaveImages(bpy.types.Operator):
'''Save all images'''
bl_idname = "wm.save_images"
bl_label = "Save all images"

def invoke(self, context, event):
correct = 0
for img in bpy.data.images:
try:
img.save()
correct += 1
except:
# some images don't have a source path (e.g. render result)
pass

self.report({'INFO'}, "Saved " + str(correct) + " images")

return {'FINISHED'}



class StraightLine(bpy.types.Operator):
'''Paint a straight line'''
bl_idname = "paint.straight_line"
bl_label = "Straight line"
time = 0
stroke = []

def to_stroke_elem(self, x, y):

elem = {"name":"",
"pen_flip":False,
"is_start":False,
"location":(0,0,0),
"mouse":(x, y),
"pressure":1,
"time": self.time
}

return elem

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def modal(self, context, event):
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
#context.region.callback_remove(self.handle)
bpy.types.SpaceView3D.draw_handler_remove(self._v3dhandle, 'WINDOW')
bpy.types.SpaceImageEditor.draw_handler_remove(self._imghandle, 'WINDOW')
#Hrm, since the paint operator paints only 2 dots with 2 stroke elems
#need to interpolate the rest to be between 1st and last, BAD...
s = [self.stroke[0], self.stroke[-1]]
start_x = s[0]["mouse"][0]
start_y = s[0]["mouse"][1]
length_x = s[1]["mouse"][0] - start_x
length_y = s[1]["mouse"][1] - start_y
length = int(math.sqrt( length_x * length_x + length_y * length_y ) / 2.0)
stroke = []
self.time = 0.0
#print( "INFO>>> processing " + str(length) + " units length..." )
if (length <= 0):
self.report({'INFO'}, "Mouse movement too short to process")
return {'FINISHED'}

# the following is an attempt to reduce the "spottiness" caused by
# multiple brush placements... by brushing the path slowly both ways.
#
for i in range(length):
r = i / length;
x = start_x + length_x * r;
y = start_y + length_y * r;
self.time += 1.0
stroke.append( self.to_stroke_elem( x, y ) )
for i in range(length):
r = (length - i - 1) / length
x = start_x + length_x * r;
y = start_y + length_y * r;
self.time += 1.0
stroke.append( self.to_stroke_elem( x, y ) )
stroke[0]["is_start"] = True
#print( "INFO>>> painting stroke with " + str(len(stroke)) + " elements..." )
bpy.ops.paint.image_paint(stroke=stroke)
self.time = 0
self.stroke = []
context.window_manager.tpp.line_last = True
context.window_manager.tpp.line_x = s[1]["mouse"][0]
context.window_manager.tpp.line_y = s[1]["mouse"][1]
return {'FINISHED'}

elif event.type == 'MOUSEMOVE':
if event.ctrl:
x, y = do_snap( self.stroke[0]["mouse"][0],
self.stroke[0]["mouse"][1],
event.mouse_region_x,
event.mouse_region_y )
else:
x = event.mouse_region_x
y = event.mouse_region_y

self.stroke[-1]["mouse"] = (x, y)

if context.area:
context.area.tag_redraw()

return {'RUNNING_MODAL'}

def invoke(self, context, event):
self.stroke = []
if (event.alt and context.window_manager.tpp.line_last):
elem = self.to_stroke_elem( context.window_manager.tpp.line_x,
context.window_manager.tpp.line_y )
else:
context.window_manager.tpp.line_last = False
elem = self.to_stroke_elem(event.mouse_region_x,
event.mouse_region_y)
elem["is_start"] = True
self.stroke.append(copy.deepcopy(elem))
elem["is_start"] = False
self.stroke.append(elem)
context.window_manager.modal_handler_add(self)
# this was the "old" (pre ~2.66) way...
#self.handle = context.region.callback_add(draw_callback,
# (self, context), 'POST_PIXEL')
args = (self, context)
self._v3dhandle = bpy.types.SpaceView3D.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_PIXEL')
self._imghandle = bpy.types.SpaceImageEditor.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_PIXEL')
return {'RUNNING_MODAL'}


class SyncSelection(bpy.types.Operator):
'''Sync selection from uv-editor to 3d-view'''
bl_idname = "uv.sync_selection"
bl_label = "Sync selection"

_timer = None
_selection_3d = []
handle1 = None
handle2 = None
handle3 = None
area = None
region = None
overlay_vertices = []
overlay_edges = []
overlay_faces = []
position_vertices = []
position_edges = []
position_faces = []
position2_vertices = []
position2_edges = []
position2_edges = []

@classmethod
def poll(cls, context):
return(context.active_object and context.active_object.mode=='EDIT')

def modal(self, context, event):
if self.area:
self.area.tag_redraw()
if context.area:
context.area.tag_redraw()

if context.window_manager.tpp.sync_enabled == -1:
self.region.callback_remove(self.handle1)
self.region.callback_remove(self.handle2)
context.region.callback_remove(self.handle3)
self.area = None
self.region = None
context.window_manager.tpp.sync_enabled = 0
return {"CANCELLED"}

return {'PASS_THROUGH'}

def invoke(self, context, event):
if context.window_manager.tpp.sync_enabled < 1:
for area in context.screen.areas:
if area.type == 'VIEW_3D':
self.area = area
for region in area.regions:
if region.type == 'WINDOW':
self.region = region
context.window_manager.tpp.sync_enabled = 1

# getting overlay selection
old_sync = context.tool_settings.use_uv_select_sync
old_select_mode = [x for x in context.tool_settings.mesh_select_mode]
context.tool_settings.mesh_select_mode = [True, False, False]
bpy.ops.object.mode_set(mode='OBJECT')
mesh = context.active_object.data
self._selection_3d = [v.index for v in mesh.vertices if v.select]
tfl = mesh.uv_textures.active
selected = []
for mface, tface in zip(mesh.faces, tfl.data):
selected += [mface.vertices[i] for i, x in enumerate(tface.select_uv) if x]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
context.tool_settings.use_uv_select_sync = True
for v in selected:
mesh.vertices[v].select = True

bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')

# indices for overlay in 3d-view
self.overlay_vertices = [vertex.index for vertex in mesh.vertices if vertex.select]
self.overlay_edges = [edge.index for edge in mesh.edges if edge.select]
self.overlay_faces = [face.index for face in mesh.faces if face.select]

# overlay positions for image editor
dict_vertex_pos = dict([[i, []] for i in range(len(mesh.vertices))])
tfl = mesh.uv_textures.active
for mface, tface in zip(mesh.faces, tfl.data):
for i, vert in enumerate(mface.vertices):
dict_vertex_pos[vert].append([co for co in tface.uv[i]])

self.position2_vertices = []
for v in self.overlay_vertices:
for pos in dict_vertex_pos[v]:
self.position2_vertices.append(pos)

# set everything back to original state
bpy.ops.object.mode_set(mode='EDIT')
context.tool_settings.use_uv_select_sync = old_sync
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
for v in self._selection_3d:
mesh.vertices[v].select = True
bpy.ops.object.mode_set(mode='EDIT')
context.tool_settings.mesh_select_mode = old_select_mode


# 3d view callbacks
context.window_manager.modal_handler_add(self)
self.handle1 = region.callback_add(sync_calc_callback,
(self, context, area, region), "POST_VIEW")
self.handle2 = region.callback_add(sync_draw_callback,
(self, context), "POST_PIXEL")

# image editor callback
self.handle3 = context.region.callback_add(sync_draw_callback2,
(self, context), "POST_VIEW")

break
break
else:
context.window_manager.tpp.sync_enabled = -1

return {'RUNNING_MODAL'}


class ToggleAddMultiply(bpy.types.Operator):
'''Toggle between Add and Multiply blend modes'''
bl_idname = "paint.toggle_add_multiply"
bl_label = "Toggle add/multiply"

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def invoke(self, context, event):
brush = context.tool_settings.image_paint.brush
if brush.blend != 'ADD':
brush.blend = 'ADD'
else:
brush.blend = 'MUL'

return {'FINISHED'}


class ToggleAlphaMode(bpy.types.Operator):
'''Toggle between Add Alpha and Erase Alpha blend modes'''
bl_idname = "paint.toggle_alpha_mode"
bl_label = "Toggle alpha mode"

@classmethod
def poll(cls, context):
return bpy.ops.paint.image_paint.poll()

def invoke(self, context, event):
brush = context.tool_settings.image_paint.brush
if brush.blend != 'ERASE_ALPHA':
brush.blend = 'ERASE_ALPHA'
else:
brush.blend = 'ADD_ALPHA'

return {'FINISHED'}


class ToggleImagePaint(bpy.types.Operator):
'''Toggle image painting in the UV/Image editor'''
bl_idname = "paint.toggle_image_paint"
bl_label = "Image Painting"

@classmethod
def poll(cls, context):
return(context.space_data.type == 'IMAGE_EDITOR')

def invoke(self, context, event):
if (context.space_data.mode == 'VIEW'):
context.space_data.mode = 'PAINT'
elif (context.space_data.mode == 'PAINT'):
context.space_data.mode = 'MASK'
elif (context.space_data.mode == 'MASK'):
context.space_data.mode = 'VIEW'

return {'FINISHED'}

class ToggleUVSelectSync(bpy.types.Operator):
'''Toggle use_uv_select_sync in the UV editor'''
bl_idname = "uv.toggle_uv_select_sync"
bl_label = "UV Select Sync"

@classmethod
def poll(cls, context):
return(context.space_data.type == 'IMAGE_EDITOR')

def invoke(self, context, event):
context.tool_settings.use_uv_select_sync = not context.tool_settings.use_uv_select_sync

return {'FINISHED'}


##########################################
# #
# User interface #
# #
##########################################

# panel with consolidate image options
class VIEW3D_PT_tools_consolidate(bpy.types.Panel):
bl_label = "Consolidate Images"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'

def draw(self, context):
props = context.window_manager.tpp
layout = self.layout

col = layout.column(align=True)
col.prop(props, "consolidate_filename", text="")
col.prop(props, "consolidate_fileformat", text="")
col = layout.column(align=True)
col.prop(props, "consolidate_input", text="")
row = col.row()
col_left = row.column(align=True)
col_left.active = not props.consolidate_auto_size
col_right = row.column()
col_left.prop(props, "consolidate_width")
col_left.prop(props, "consolidate_height")
col_right.prop(props, "consolidate_auto_size")
col_right.prop(props, "consolidate_alpha")
layout.prop(props, "consolidate_split")
col = layout.column(align=True)
col.operator("image.consolidate", text="Consolidate")
col.operator("image.consolidate", text="Consolidate + remap").remap = True


# property group containing all properties of the add-on
class TexturePaintPlusProps(bpy.types.PropertyGroup):
consolidate_alpha = bpy.props.BoolProperty(name = "Alpha",
description = "Determines if consolidated image has an alpha channel",
default = True)
consolidate_auto_size = bpy.props.BoolProperty(name = "Auto",
description = "Automatically determine size of consolidated image, no resizing is done",
default = False)
consolidate_fileformat = bpy.props.EnumProperty(name = "Fileformat",
items = (("BMP", "BMP", "Output image in bitmap format"),
("IRIS", "Iris", "Output image in (old!) SGI IRIS format"),
("PNG", "PNG", "Output image in PNG format"),
("JPEG", "JPEG", "Output image in JPEG format"),
("TARGA", "Targa", "Output image in Targa format"),
("TARGA_RAW", "Targa Raw", "Output image in uncompressed Targa format"),
("CINEON", "Cineon", "Output image in Cineon format"),
("DPX", "DPX", "Output image in DPX format"),
("MULTILAYER", "MultiLayer", "Output image in multilayer OpenEXR format"),
("OPEN_EXR", "OpenEXR", "Output image in OpenEXR format"),
("HDR", "Radiance HDR", "Output image in Radiance HDR format"),
("TIFF", "TIFF", "Output image in TIFF format")),
description = "File format to save the rendered image as",
default = 'TARGA')
consolidate_filename = bpy.props.StringProperty(name = "Filename",
description = "Location where to store the consolidated image",
default = "//consolidate",
subtype = 'FILE_PATH')
consolidate_height = bpy.props.IntProperty(name = "Y",
description = "Image height",
min = 1,
default = 1024)
consolidate_input = bpy.props.EnumProperty(name = "Input",
items = (("active", "Only active object", "Only combine the images of the active object"),
("selected", "All selected objects", "Combine the images of all selected objects")),
description = "Objects of which the images will be consolidated into a single image",
default = 'selected')
consolidate_split = bpy.props.BoolProperty(name = "Split by type",
description = "Consolidate Diffuse, Shading, Specular and Geometry influences into different images",
default = True)
consolidate_width = bpy.props.IntProperty(name = "X",
description = "Image width",
min = 1,
default = 1024)
sync_enabled = bpy.props.IntProperty(name = "Enabled",
description = "internal use",
default = 0)
toolmode_enabled = bpy.props.IntProperty(name = "Enabled",
description = "internal use",
default = 0)
toolmode_mode = bpy.props.StringProperty(name = "Mode",
description = "internal use",
default = "")
toolmode_tool = bpy.props.StringProperty(name = "Tool",
description = "internal use",
default = "")
line_last = bpy.props.BoolProperty(name = "Last_f",
description = "Last position valid",
default = False)
line_x = bpy.props.IntProperty(name = "Last_x",
description = "Last position X",
default = 0)
line_y = bpy.props.IntProperty(name = "Last_y",
description = "Last position y",
default = 0)



classes = [AddDefaultImage,
AutoMergeUV,
BrushPopup,
ChangeSelection,
ConsolidateImages,
# CycleBlendtype,
DefaultMaterial,
DisplayToolMode,
GridTexture,
MassLinkAppend,
OriginSet,
ReloadImage,
ReloadImages,
SampleColor,
SaveImage,
SaveImages,
StraightLine,
SyncSelection,
ToggleAddMultiply,
ToggleAlphaMode,
ToggleImagePaint,
ToggleUVSelectSync,
VIEW3D_PT_tools_consolidate,
TexturePaintPlusProps]


def menu_func(self, context):
layout = self.layout
wm = context.window_manager
if "tpp_automergeuv" not in wm:
automergeuv_enabled = False
else:
automergeuv_enabled = wm["tpp_automergeuv"]

if automergeuv_enabled:
layout.operator("paint.auto_merge_uv", icon="CHECKBOX_HLT")
else:
layout.operator("paint.auto_merge_uv", icon="CHECKBOX_DEHLT")


def menu_mesh_select_mode(self, context):
layout = self.layout
layout.separator()

prop = layout.operator("wm.context_set_value", text="Vertex + Edge", icon='EDITMODE_HLT')
prop.value = "(True, True, False)"
prop.data_path = "tool_settings.mesh_select_mode"

prop = layout.operator("wm.context_set_value", text="Vertex + Face", icon='ORTHO')
prop.value = "(True, False, True)"
prop.data_path = "tool_settings.mesh_select_mode"

prop = layout.operator("wm.context_set_value", text="Edge + Face", icon='SNAP_FACE')
prop.value = "(False, True, True)"
prop.data_path = "tool_settings.mesh_select_mode"

layout.separator()

prop = layout.operator("wm.context_set_value", text="All", icon='OBJECT_DATAMODE')
prop.value = "(True, True, True)"
prop.data_path = "tool_settings.mesh_select_mode"


def menu_snap(self, context):
layout = self.layout
layout.separator()

layout.operator("mesh.origin_set", text="Geometry to Origin")
layout.operator("mesh.origin_set", text="Origin to Geometry").type = 'ORIGIN_GEOMETRY'
layout.operator("mesh.origin_set", text="Origin to 3D Cursor").type = 'ORIGIN_CURSOR'


def register():
import bpy
# register classes
init_props()
for c in classes:
bpy.utils.register_class(c)
bpy.types.WindowManager.tpp = bpy.props.PointerProperty(\
type = TexturePaintPlusProps)

# add Image Paint keymap entries
km = bpy.context.window_manager.keyconfigs.default.keymaps['Image Paint']
kmi = km.keymap_items.new("paint.toggle_alpha_mode", 'A', 'PRESS')
# kmi = km.keymap_items.new("paint.cycle_blendtype", 'A', 'PRESS',
# ctrl=True)
kmi = km.keymap_items.new("wm.context_toggle", 'B', 'PRESS')
kmi.properties.data_path = "user_preferences.system.use_mipmaps"
kmi = km.keymap_items.new("paint.toggle_add_multiply", 'D', 'PRESS')
kmi = km.keymap_items.new("paint.display_tool_mode", 'F', 'PRESS',
ctrl=True)
kmi = km.keymap_items.new("paint.grid_texture", 'G', 'PRESS')
kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
shift=True)
kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
shift=True, ctrl=True)
kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
shift=True, alt=True)
kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
shift=True, ctrl=True, alt=True)
kmi = km.keymap_items.new("paint.change_selection", 'NUMPAD_MINUS', 'PRESS',
ctrl=True)
kmi.properties.mode = 'less'
kmi = km.keymap_items.new("paint.change_selection", 'NUMPAD_PLUS', 'PRESS',
ctrl=True)
kmi.properties.mode = 'more'
kmi = km.keymap_items.new("wm.context_set_enum", 'Q', 'PRESS')
kmi.properties.data_path = "tool_settings.image_paint.brush.blend"
kmi.properties.value = 'MIX'
kmi = km.keymap_items.new("wm.context_toggle", 'R', 'PRESS')
kmi.properties.data_path = "active_object.show_wire"
kmi = km.keymap_items.new("paint.reload_image", 'R', 'PRESS',
alt=True)
# kmi = km.keymap_items.new("paint.sample_color_custom", 'RIGHTMOUSE', 'PRESS')
kmi = km.keymap_items.new("wm.context_toggle", 'S', 'PRESS')
kmi.properties.data_path = "active_object.data.use_paint_mask"
kmi = km.keymap_items.new("paint.save_image", 'S', 'PRESS',
alt=True)
kmi = km.keymap_items.new("paint.brush_popup", 'W', 'PRESS')
kmi = km.keymap_items.new("paint.toggle_image_paint", 'W', 'PRESS',
shift=True)

# add 3D View keymap entry
km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
kmi = km.keymap_items.new("object.add_default_image", 'Q', 'PRESS')
kmi = km.keymap_items.new("object.default_material", 'X', 'PRESS',
alt=True, ctrl=True)

# add Mesh keymap entry
km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
# kmi = km.keymap_items.new("paint.cycle_blendtype", 'A', 'PRESS',
# ctrl=True)

# print(bpy.context.window_manager.keyconfigs.default.keymaps.keys())
# add UV Editor keymap entries, somehow UV Editor is not found, hrm...
# km = bpy.context.window_manager.keyconfigs.default.keymaps['UV Editor']
# kmi = km.keymap_items.new("uv.sync_selection", 'F', 'PRESS')
# kmi = km.keymap_items.new("uv.toggle_uv_select_sync", 'F', 'PRESS',
# shift=True)
# kmi = km.keymap_items.new("wm.context_menu_enum", 'TAB', 'PRESS',
# ctrl=True, shift=True)
# kmi = km.keymap_items.new("paint.toggle_image_paint", 'W', 'PRESS',
# shift=True)

# deactivate to prevent clashing
km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
for kmi in km.keymap_items:
if kmi.type == 'S' and not kmi.any and not kmi.shift and kmi.ctrl and kmi.alt and not kmi.oskey:
kmi.active = False

# add Window keymap entry
km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
kmi = km.keymap_items.new("wm.mass_link_append", 'F1', 'PRESS',
alt=True)
kmi = km.keymap_items.new("paint.reload_images", 'R', 'PRESS',
alt=True, ctrl=True)
kmi = km.keymap_items.new("wm.save_images", 'S','PRESS',
alt=True, ctrl=True)

# deactivate and remap to prevent clashing
if bpy.context.user_preferences.inputs.select_mouse == 'RIGHT':
right_mouse = ['RIGHTMOUSE', 'SELECTIONMOUSE']
else: #'LEFT'
right_mouse = ['RIGHTMOUSE', 'ACTIONMOUSE']
km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
for kmi in km.keymap_items:
if kmi.type in right_mouse and kmi.alt and not kmi.ctrl and not kmi.shift:
# deactivate
kmi.active = False
for kmi in km.keymap_items:
if kmi.type in right_mouse and not kmi.alt and not kmi.ctrl and not kmi.shift:
# remap
kmi.alt = True

# add menu entries
bpy.types.VIEW3D_MT_edit_mesh.prepend(menu_func)
bpy.types.VIEW3D_MT_edit_mesh_select_mode.append(menu_mesh_select_mode)
bpy.types.VIEW3D_MT_snap.append(menu_snap)


def unregister():
# remove menu entries
bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)
bpy.types.VIEW3D_MT_edit_mesh_select_mode.remove(menu_mesh_select_mode)
bpy.types.VIEW3D_MT_snap.remove(menu_snap)

# remove Image Paint keymap entries
km = bpy.context.window_manager.keyconfigs.default.keymaps['Image Paint']
for kmi in km.keymap_items:
if kmi.idname in ["paint.brush_popup", "paint.straight_line", "paint.toggle_alpha_mode", "paint.sample_color_custom", "paint.change_selection", "paint.cycle_blendtype", "paint.toggle_image_paint",
"paint.toggle_add_multiply", "paint.grid_texture", "paint.display_tool_mode", "paint.reload_image", "paint.save_image"]:
km.keymap_items.remove(kmi)
elif kmi.idname == "wm.context_toggle":
if getattr(kmi.properties, "data_path", False) in ["active_object.data.use_paint_mask", "active_object.show_wire", "user_preferences.system.use_mipmaps"]:
km.keymap_items.remove(kmi)
elif kmi.idname == "wm.context_set_enum":
if getattr(kmi.properties, "data_path", False) in ["tool_settings.image_paint.brush.blend"]:
km.keymap_items.remove(kmi)

# remove 3D View keymap entry
km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
for kmi in km.keymap_items:
if kmi.idname in ["object.add_default_image", "object.default_material"]:
km.keymap_items.remove(kmi)

# remove Mesh keymap entry
km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
for kmi in km.keymap_items:
if kmi.idname in ["paint.cycle_blendtype"]:
km.keymap_items.remove(kmi)

# remove UV Editor keymap entries
#km = bpy.context.window_manager.keyconfigs.default.keymaps['UV Editor']
#for kmi in km.keymap_items:
# if kmi.idname in ["wm.context_menu_enum", "uv.sync_selection", "uv.toggle_uv_select_sync", "paint.toggle_image_paint"]:
# km.keymap_items.remove(kmi)

# remove Window keymap entry
#km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
#for kmi in km.keyamp_items:
# if kmi.idname in ["wm.mass_link_append", "paint.reload_images", "wm.save_images"]:
# km.keymap_items.remove(kmi)

# remap and reactivate original items
if bpy.context.user_preferences.inputs.select_mouse == 'RIGHT':
right_mouse = ['RIGHTMOUSE', 'SELECTIONMOUSE']
else: #'LEFT'
right_mouse = ['RIGHTMOUSE', 'ACTIONMOUSE']
km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
for kmi in km.keymap_items:
if kmi.type in right_mouse and kmi.alt and not kmi.ctrl and not kmi.shift:
if kmi.active:
# remap
kmi.alt = False
else:
# reactivate
kmi.active = True

# reactive original item
km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
for kmi in km.keymap_items:
if kmi.type == 'S' and not kmi.any and not kmi.shift and kmi.ctrl and kmi.alt and not kmi.oskey:
kmi.active = True

# unregister classes
remove_props()
for c in classes:
bpy.utils.unregister_class(c)
try:
del bpy.types.WindowManager.tpp
except:
pass


if __name__ == "__main__":
register()
  1. bl_info = {
  2.     'name': 'Texture Paint plus',
  3.     'author': 'Bart Crouch, scorpion81 (straight line fix)',
  4.     'version': (1, 28),
  5.     'blender': (2, 6, 9),
  6.     'location': 'View3D > Texture Paint mode',
  7.     'warning': '',
  8.     'description': 'Several improvements for Texture Paint mode',
  9.     'wiki_url': '',
  10.     'tracker_url': '',
  11.     'category': 'Paint'}
  12.  
  13.  
  14. import bgl
  15. import blf
  16. import bpy
  17. import mathutils
  18. import os
  19. import time
  20. import copy
  21. import math
  22. from bpy_extras.io_utils import ImportHelper
  23.  
  24.  
  25. ##########################################
  26. #                                        #
  27. # Functions                              #
  28. #                                        #
  29. ##########################################
  30.  
  31. # check if any of the images has to be resized
  32. def consolidate_check_resize(image_map):
  33.     resized = False
  34.     for [image, [x_min, y_min, x_max, y_max]] in image_map:
  35.         width = x_max - x_min
  36.         height = y_max - y_min
  37.         if width != image.size[0] or height != image.size[1]:
  38.             resized = True
  39.             break
  40.        
  41.     return(resized)
  42.  
  43.  
  44. # create a new image that contains all other images
  45. def consolidate_copy_images(width, height, alpha, image_map, filename, fileformat, restrict_name):
  46.     # get image name
  47.     old_format = bpy.context.scene.render.image_settings.file_format
  48.     bpy.context.scene.render.image_settings.file_format = fileformat
  49.     extension = bpy.context.scene.render.file_extension
  50.     bpy.context.scene.render.image_settings.file_format = old_format
  51.     if len(extension) > len(filename) and filename[-len(extension):].lower() == extension.lower():
  52.         filename = filename[:-len(extension)]
  53.     if restrict_name != "consolidate":
  54.         filename += "_" + restrict_name
  55.    
  56.     width = int(width)
  57.     height = int(height)
  58.     composite_image = bpy.data.images.new(filename, width, height, alpha=alpha)
  59.     composite_image.filepath_raw = filename + extension
  60.     composite_image.file_format = fileformat
  61.    
  62.     composite_buffer = ImageBuffer(composite_image)
  63.     for [image, [x_offset, y_offset, x_max, y_max]] in image_map:
  64.         buffer = ImageBuffer(image)
  65.         for x in range(image.size[0]):
  66.             for y in range(image.size[1]):
  67.                 composite_buffer.set_pixel(x+x_offset, y+y_offset, buffer.get_pixel(x, y))
  68.     composite_buffer.update()
  69.    
  70.     # save and reload
  71.     composite_image.save()
  72.     bpy.data.images.remove(composite_image)
  73.     composite_image = bpy.data.images.load(filename + extension)
  74.    
  75.     return(composite_image)
  76.  
  77.  
  78. # retrieve list of images and associated data
  79. def consolidate_get_images(meshes, restrict_values, all_ts):
  80.     images = []
  81.     textures = []
  82.     texture_slots = []
  83.     uv_layers = []
  84.    
  85.     def is_within_restriction(texture_slot):
  86.         if not restrict_values:
  87.             return True
  88.         for value in restrict_values:
  89.             if getattr(texture_slot, value, False):
  90.                 return True
  91.         return False
  92.    
  93.     for mesh in meshes:
  94.         # scan texture slots
  95.         ts_found = False
  96.         for mat in mesh.materials:
  97.             if not mat:
  98.                 continue
  99.             for ts in [ts for ts in mat.texture_slots if ts and ts.texture.type=='IMAGE']:
  100.                 if ts.texture_coords == 'UV':
  101.                     ts_found = True
  102.                     if not is_within_restriction(ts):
  103.                         continue
  104.                     if ts in all_ts:
  105.                         # texture slot already done in different consolidated image
  106.                         continue
  107.                     else:
  108.                         texture_slots.append(ts)
  109.                     image = ts.texture.image
  110.                     if image not in images:
  111.                         images.append(image)
  112.                     if ts.texture not in textures:
  113.                         textures.append(ts.texture)
  114.                     uv_layer = ts.uv_layer
  115.                     if not uv_layer:
  116.                         uv_layer = mesh.uv_textures.active
  117.                     else:
  118.                         uv_layer = mesh.uv_textures[uv_layer]
  119.                     uv_layers.append([mesh, image, mat, ts, uv_layer])
  120.         if not ts_found:
  121.             # scan game property assignments
  122.             # not possible anymore since these settings have been moved
  123.             pass
  124. #            for tex in mesh.uv_textures:
  125. #                for face in tex.data:
  126. #                    if face.use_image:
  127. #                        if face.image not in images:
  128. #                            images.append(face.image)
  129. #                            uv_layers.append([mesh, face.image, False, False, tex])
  130.                    
  131.     return(images, textures, texture_slots, ts_found, uv_layers)
  132.  
  133.  
  134. # list of meshes for Consolidate Images operator
  135. def consolidate_get_input(input):
  136.     if input == 'active':
  137.         meshes = []
  138.         ob = bpy.context.active_object
  139.         if ob and ob.type == 'MESH':
  140.             mesh = ob.data
  141.             if len(mesh.uv_textures) > 0:
  142.                 meshes.append(bpy.context.active_object.data)
  143.     else: # input == 'selected'
  144.         meshes = [ob.data for ob in bpy.context.selected_objects if ob.type=='MESH' and len(ob.data.uv_textures)>0]
  145.        
  146.     return(meshes)
  147.  
  148.  
  149. # create a new image that contains all other images
  150. def consolidate_new_image(width, height, alpha, image_map, filename, fileformat, restrict_name):
  151.     # create temporary scene
  152.     temp_scene = bpy.data.scenes.new("temp_scene")
  153.     temp_world = bpy.data.worlds.new("temp_world")
  154.     temp_scene.world = temp_world
  155.    
  156.     # setup the camera
  157.     cam = bpy.data.cameras.new("temp_cam_ob")
  158.     cam.type = 'ORTHO'
  159.     cam.ortho_scale = 1.0
  160.     obj_cam = bpy.data.objects.new("temp_cam", cam)
  161.     obj_cam.location = [0.5, 0.5, 1.0]
  162.     if width < height:
  163.         obj_cam.location[0] = (width/height) / 2
  164.     elif height < width:
  165.         obj_cam.location[1] = (height/width) / 2
  166.     temp_scene.objects.link(obj_cam)
  167.     temp_scene.camera = obj_cam
  168.     obj_cam.layers[0] = True
  169.        
  170.     # render settings
  171.     temp_scene.render.use_raytrace = False
  172.     temp_scene.render.alpha_mode = 'STRAIGHT'
  173.     if alpha:
  174.         temp_scene.render.image_settings.color_mode = 'RGBA'
  175.     else:
  176.         temp_scene.render.image_settings.color_mode = 'RGB'
  177.     temp_scene.render.resolution_x = width
  178.     temp_scene.render.resolution_y = height
  179.     temp_scene.render.resolution_percentage = 100
  180.     temp_scene.render.pixel_aspect_x = 1.0
  181.     temp_scene.render.pixel_aspect_y = 1.0
  182.     temp_scene.render.image_settings.file_format = fileformat
  183.     extension = temp_scene.render.file_extension
  184.     if len(extension) > len(filename) and filename[-len(extension):].lower() == extension.lower():
  185.         filename = filename[:-len(extension)]
  186.     if restrict_name != "consolidate":
  187.         filename += "_" + restrict_name
  188.     temp_scene.render.filepath = filename
  189.     temp_scene.render.antialiasing_samples = '16'
  190.     temp_scene.render.pixel_filter_type = 'MITCHELL'
  191.     temp_scene.render.use_file_extension = True
  192.     temp_scene.render.use_overwrite = True
  193.        
  194.         # materials
  195.     total_object = [0, 0]
  196.     for [img, [x0, y0, x1, y1]] in image_map:
  197.         if x0 > total_object[0]:
  198.             total_object[0] = x0
  199.         if y0 > total_object[1]:
  200.             total_object[1] = y0
  201.         if x1 > total_object[0]:
  202.             total_object[0] = x1
  203.         if y1 > total_object[1]:
  204.             total_object[1] = y1
  205.     xtotal, ytotal = total_object
  206.    
  207.     temp_materials = [[bpy.data.materials.new("temp_mat"), x0, y0, x1, y1] for [img, [x0, y0, x1, y1]] in image_map]
  208.     temp_textures = [bpy.data.textures.new("temp_tex", 'IMAGE') for i in range(len(image_map))]
  209.     for i, [temp_mat, x0, y0, x1, y1] in enumerate(temp_materials):
  210.         temp_mat.use_shadeless = True
  211.         temp_mat.use_face_texture = True
  212.         temp_mat.use_face_texture_alpha = True
  213.         temp_mat.use_transparency = True
  214.         temp_mat.transparency_method = 'Z_TRANSPARENCY'
  215.         temp_mat.alpha = 1.0
  216.         temp_mat.texture_slots.add()
  217.         temp_mat.texture_slots[0].texture = temp_textures[i]
  218.         temp_mat.texture_slots[0].texture.image = image_map[i][0]
  219.         # texture mapping
  220.         xlength = x1 - x0
  221.         xzoom = xtotal/(xlength)
  222.         xoriginoffset = 0.5 - (0.5 / xzoom)
  223.         xtargetoffset = x0 / xtotal
  224.         xoffset = (xtargetoffset - xoriginoffset) * -xzoom
  225.         ylength = y1 - y0
  226.         yzoom = ytotal/(ylength)
  227.         yoriginoffset = 0.5 - (0.5 / yzoom)
  228.         ytargetoffset = y0 / ytotal
  229.         yoffset = (ytargetoffset - yoriginoffset) * -yzoom
  230.         temp_mat.texture_slots[0].offset = [xoffset, yoffset, 0]
  231.         temp_mat.texture_slots[0].scale = [xzoom, yzoom, 1]
  232.    
  233.     # mesh
  234.     temp_mesh = bpy.data.meshes.new("temp_mesh")
  235.     for [temp_mat, x0, y0, x1, y1] in temp_materials:
  236.         temp_mesh.materials.append(temp_mat)
  237.    
  238.     temp_obj = bpy.data.objects.new("temp_object", temp_mesh)
  239.     temp_scene.objects.link(temp_obj)
  240.     temp_obj.layers[0] = True
  241.    
  242.     new_vertices = []
  243.     new_faces = []
  244.     i = 0
  245.     for [img, [x0, y0, x1, y1]] in image_map:
  246.         new_vertices.extend([x0, y0, 0, x1, y0, 0, x1, y1, 0, x0, y1, 0])
  247.         new_faces.extend([i, i+1, i+2, i+3])
  248.         i += 4
  249.     temp_mesh.vertices.add(i)
  250.     temp_mesh.faces.add(i//4)
  251.     temp_mesh.vertices.foreach_set('co', new_vertices)
  252.     temp_mesh.faces.foreach_set('vertices_raw', new_faces)
  253.     temp_mesh.faces.foreach_set("material_index", range(i//4))
  254.     temp_mesh.update(calc_edges=True)
  255.     max_size = max(width, height)
  256.     temp_obj.scale = [1/max_size, 1/max_size, 1]
  257.    
  258.     # render composite image
  259.     data_context = {"blend_data": bpy.context.blend_data, "scene": temp_scene}
  260.     bpy.ops.render.render(data_context, write_still=True)
  261.    
  262.     # cleaning
  263.     bpy.data.scenes.remove(temp_scene)
  264.     bpy.data.objects.remove(obj_cam)
  265.     bpy.data.cameras.remove(cam)
  266.     bpy.data.objects.remove(temp_obj)
  267.     bpy.data.meshes.remove(temp_mesh)
  268.     for [temp_mat, x0, y0, x1, y1] in temp_materials:
  269.         bpy.data.materials.remove(temp_mat)
  270.     for temp_tex in temp_textures:
  271.         bpy.data.textures.remove(temp_tex)
  272.    
  273.     # load composite image into blender's database
  274.     composite_image = bpy.data.images.load(filename + extension)
  275.    
  276.     return(composite_image)
  277.  
  278.  
  279. # bin pack images
  280. def consolidate_pack_images(images, image_width, image_height, auto_size):
  281.     image_sizes = [[image.size[0]*image.size[1], image.size[0], image.size[1], image.name, image] for image in images]
  282.     image_sizes.sort()
  283.     total_area = sum([size[0] for size in image_sizes])
  284.     container_zoom = 1.5
  285.     zoom_delta = .1
  286.     fit = 0 # 0 = no fit tried yet, 1 = fit found, 2 = container too small
  287.     searching = True
  288.     image_map = []
  289.     iteration = 0
  290.    
  291.     while searching:
  292.         iteration += 1
  293.         if iteration % 5 == 0:
  294.             # increase the container increase, to speed things up
  295.             zoom_delta *= 2
  296.         temp_map = []
  297.         success = True
  298.         image_area = image_width * image_height
  299.         image_zoom = total_area / image_area
  300.         container_width = image_width * image_zoom * container_zoom
  301.         container_height = image_height * image_zoom * container_zoom
  302.         tree = PackTree([container_width, container_height])
  303.         for area, width, height, name, image in image_sizes:
  304.             uv = tree.insert([width, height])
  305.             if not uv:
  306.                 success = False
  307.                 if fit == 1:
  308.                     searching = False
  309.                     break
  310.                 fit = 2
  311.                 container_zoom += zoom_delta
  312.                 break
  313.             temp_map.append([image, uv.area])
  314.         if success:
  315.             fit = 1
  316.             image_map = temp_map
  317.             container_zoom -= zoom_delta
  318.    
  319.     width = 0
  320.     height = 0
  321.     for image, [x_min, y_min, x_max, y_max] in image_map:
  322.         if x_max > width:
  323.             width = x_max
  324.         if y_max > height:
  325.             height = y_max
  326.    
  327.     zoom = max([(width / image_width), (height / image_height)])
  328.     if auto_size:
  329.         image_width *= zoom
  330.         image_height *= zoom
  331.     else:
  332.         image_map = [[image, [x_min/zoom, y_min/zoom, x_max/zoom, y_max/zoom]] for image, [x_min, y_min, x_max, y_max] in image_map]
  333.    
  334.     return(image_map, image_width, image_height)
  335.  
  336.  
  337. def consolidate_update_textures(textures, uv_layers, image_map, composite_image, restrict_name):
  338.     # create remapped UV layers
  339.     total_width, total_height = composite_image.size
  340.     for [mesh, image, mat, ts, uv_layer] in uv_layers:
  341.         layer_names = [layer.name for layer in mesh.uv_textures]
  342.         new_name = uv_layer.name[:min(8, len(uv_layer.name))]
  343.         if restrict_name == "consolidate":
  344.             new_name += "_remap"
  345.         else:
  346.             new_name += "_" + restrict_name
  347.         if new_name in layer_names:
  348.             # problem, as we need a unique name
  349.             unique = False
  350.             n = 1
  351.             while not unique:
  352.                 if new_name + "." + str(n).rjust(3, "0") not in layer_names:
  353.                     new_name += "." + str(n).rjust(3, "0")
  354.                     unique = True
  355.                     break
  356.                 else:
  357.                     n += 1
  358.                     if n > 999:
  359.                         # couldn't find unique name
  360.                         unique = True
  361.                         break
  362.         new_layer = mesh.uv_textures.new(new_name)
  363.         if not new_layer:
  364.             continue
  365.         for [old_image, coords] in image_map:
  366.             if image == old_image:
  367.                 break
  368.         offset_x, offset_y, x_max, y_max = coords
  369.         new_width = x_max - offset_x
  370.         new_height = y_max - offset_y
  371.         offset_x /= total_width
  372.         offset_y /= total_height
  373.         zoom_x = new_width / total_width
  374.         zoom_y = new_height / total_height
  375.         for i, mface in enumerate(uv_layer.data):
  376.             # get texture face data
  377.             pin_uv = mface.pin_uv
  378.             select_uv = mface.select_uv
  379.             uv_raw = [uv_co for uv_co in mface.uv_raw]
  380.             # remap UVs
  381.             for index in range(0, 8, 2):
  382.                 uv_raw[index] = (uv_raw[index] * zoom_x) + offset_x
  383.             for index in range(1, 8, 2):
  384.                 uv_raw[index] = (uv_raw[index] * zoom_y) + offset_y
  385.             # set texture face data
  386.             new_layer.data[i].uv_raw = uv_raw
  387.             new_layer.data[i].image = composite_image
  388.             new_layer.data[i].pin_uv = pin_uv
  389.             new_layer.data[i].select_uv = select_uv
  390.         if ts:
  391.             ts.uv_layer = new_layer.name
  392.             #ts.scale[0] /= (image.size[0] / composite_image.size[0])
  393.             #ts.scale[1] /= (image.size[1] / composite_image.size[1])
  394.         else:
  395.             mesh.uv_textures.active_index = len(mesh.uv_textures) - 1
  396.     # replace images with composite_image
  397.     for tex in textures:
  398.         tex.image = composite_image
  399.  
  400.  
  401. # draw in 3d-view
  402. def draw_callback(self, context):
  403.     r, g, b = context.tool_settings.image_paint.brush.cursor_color_add
  404.     #x0, y0, x1, y1 = context.window_manager["straight_line"]
  405.     start = self.stroke[0]
  406.     end = self.stroke[-1]
  407.    
  408.     x0 = start["mouse"][0]
  409.     y0 = start["mouse"][1]
  410.    
  411.     x1 = end["mouse"][0]
  412.     y1 = end["mouse"][1]
  413.    
  414.     # draw straight line
  415.     bgl.glEnable(bgl.GL_BLEND)
  416.     bgl.glColor4f(r, g, b, 1.0)
  417.     bgl.glBegin(bgl.GL_LINE_STRIP)
  418.     bgl.glVertex2i(x0, y0)
  419.     bgl.glVertex2i(x1, y1)
  420.     bgl.glEnd()
  421.     # restore opengl defaults
  422.     bgl.glDisable(bgl.GL_BLEND)
  423.     bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
  424.  
  425.  
  426. # return a list of all images that are being displayed in an editor
  427. def get_images_in_editors(context):
  428.     images = []
  429.     for area in context.screen.areas:
  430.         if area.type != 'IMAGE_EDITOR':
  431.             continue
  432.         for space in area.spaces:
  433.             if space.type != 'IMAGE_EDITOR':
  434.                 continue
  435.             if space.image:
  436.                 images.append(space.image)
  437.                 area.tag_redraw()
  438.    
  439.     return(images)
  440.  
  441.  
  442. # calculate for 3d-view
  443. def sync_calc_callback(self, context, area, region):
  444.     mid_x = region.width/2.0
  445.     mid_y = region.height/2.0
  446.     width = region.width
  447.     height = region.height
  448.    
  449.     region_3d = False
  450.     for space in area.spaces:
  451.         if space.type == 'VIEW_3D':
  452.             region_3d = space.region_3d
  453.     if not region_3d:
  454.         return
  455.    
  456.     view_mat = region_3d.perspective_matrix
  457.     ob_mat = context.active_object.matrix_world
  458.     total_mat = view_mat * ob_mat
  459.     mesh = context.active_object.data
  460.    
  461.     def transform_loc(loc):
  462.         vec = total_mat * loc
  463.         vec = mathutils.Vector([vec[0]/vec[3], vec[1]/vec[3], vec[2]/vec[3]])
  464.         x = int(mid_x + vec[0]*width/2.0)
  465.         y = int(mid_y + vec[1]*height/2.0)
  466.        
  467.         return([x, y])
  468.    
  469.     # vertices
  470.     locs = [mesh.vertices[v].co.to_4d() for v in self.overlay_vertices]
  471.     self.position_vertices = []
  472.     for loc in locs:
  473.         self.position_vertices.append(transform_loc(loc))
  474.    
  475.     # edges
  476.     locs = [[mesh.vertices[mesh.edges[edge].vertices[0]].co.to_4d(),
  477.         mesh.vertices[mesh.edges[edge].vertices[1]].co.to_4d()] \
  478.         for edge in self.overlay_edges]
  479.     self.position_edges = []
  480.     for v1, v2 in locs:
  481.         self.position_edges.append(transform_loc(v1))
  482.         self.position_edges.append(transform_loc(v2))
  483.    
  484.     # faces
  485.     locs = [[mesh.vertices[mesh.faces[face].vertices[0]].co.to_4d(),
  486.         mesh.vertices[mesh.faces[face].vertices[1]].co.to_4d(),
  487.         mesh.vertices[mesh.faces[face].vertices[2]].co.to_4d(),
  488.         mesh.vertices[mesh.faces[face].vertices[3]].co.to_4d(),] \
  489.         for face in self.overlay_faces]
  490.     self.position_faces = []
  491.     for v1, v2, v3, v4 in locs:
  492.         self.position_faces.append(transform_loc(v1))
  493.         self.position_faces.append(transform_loc(v2))
  494.         self.position_faces.append(transform_loc(v3))
  495.         self.position_faces.append(transform_loc(v4))
  496.    
  497.  
  498. # draw in 3d-view
  499. def sync_draw_callback(self, context):
  500.     # polling
  501.     if context.mode != "EDIT_MESH":
  502.         return
  503.    
  504.     # draw vertices
  505.     bgl.glColor4f(1.0, 0.0, 0.0, 1.0)  
  506.     bgl.glPointSize(4)
  507.     bgl.glBegin(bgl.GL_POINTS)
  508.     for x, y in self.position_vertices:
  509.         bgl.glVertex2i(x, y)
  510.     bgl.glEnd()
  511.    
  512.     # draw edges
  513.     bgl.glColor4f(1.0, 0.0, 0.0, 1.0)
  514.     bgl.glLineWidth(1.5)
  515.     bgl.glBegin(bgl.GL_LINES)
  516.     for x, y in self.position_edges:
  517.         bgl.glVertex2i(x, y)
  518.     bgl.glEnd()
  519.     bgl.glLineWidth(1)
  520.    
  521.     # draw faces
  522.     bgl.glEnable(bgl.GL_BLEND)
  523.     bgl.glColor4f(1.0, 0.0, 0.0, 0.3)
  524.     bgl.glBegin(bgl.GL_QUADS)
  525.     for x, y in self.position_faces:
  526.         bgl.glVertex2i(x, y)
  527.     bgl.glEnd()
  528.     bgl.glDisable(bgl.GL_BLEND)
  529.  
  530.  
  531. # draw in image-editor
  532. def sync_draw_callback2(self, context):
  533.     # polling
  534.     if context.mode != "EDIT_MESH":
  535.         return
  536.    
  537.     # draw vertices
  538.     bgl.glColor4f(1.0, 0.0, 0.0, 1.0)  
  539.     bgl.glPointSize(6)
  540.     bgl.glBegin(bgl.GL_POINTS)
  541.     for x, y in self.position2_vertices:
  542.         bgl.glVertex2f(x, y)
  543.     bgl.glEnd()
  544.  
  545.  
  546. # draw paint tool and blendmode in 3d-view
  547. def toolmode_draw_callback(self, context):
  548.     # polling
  549.     if context.mode != 'PAINT_TEXTURE':
  550.         return
  551.    
  552.     # draw
  553.     if context.region:
  554.         main_y = context.region.height - 32
  555.     else:
  556.         return
  557.     blend_dic = {"MIX": "Mix",
  558.         "ADD": "Add",
  559.         "SUB": "Subtract",
  560.         "MUL": "Multiply",
  561.         "LIGHTEN": "Lighten",
  562.         "DARKEN": "Darken",
  563.         "ERASE_ALPHA": "Erase Alpha",
  564.         "ADD_ALPHA": "Add Alpha"}
  565.     brush = context.tool_settings.image_paint.brush
  566.     text = brush.name + " - " + blend_dic[brush.blend]
  567.    
  568.     # text in top-left corner
  569.     bgl.glColor3f(0.6, 0.6, 0.6)
  570.     blf.position(0, 21, main_y, 0)
  571.     blf.draw(0, text)
  572.    
  573.     # text above brush
  574.     dt = time.time() - context.window_manager["tpp_toolmode_time"]
  575.     if dt < 1:
  576.         if "tpp_toolmode_brushloc" not in context.window_manager:
  577.             return
  578.         brush_x, brush_y = context.window_manager["tpp_toolmode_brushloc"]
  579.         brush_x -= blf.dimensions(0, text)[0] / 2
  580.         bgl.glColor4f(0.6, 0.6, 0.6, min(1.0, (1.0 - dt)*2))
  581.         blf.position(0, brush_x, brush_y, 0)
  582.         blf.draw(0, text)
  583.  
  584.  
  585. # add ID-properties to window-manager
  586. def init_props():
  587.     wm = bpy.context.window_manager
  588.     wm["tpp_automergeuv"] = 0
  589.  
  590.  
  591. # remove ID-properties from window-manager
  592. def remove_props():
  593.     wm = bpy.context.window_manager
  594.     if "tpp_automergeuv" in wm:
  595.         del wm["tpp_automergeuv"]
  596.     if "tpp_toolmode_time" in wm:
  597.         del wm["tpp_toolmode_time"]
  598.     if "tpp_toolmode_brushloc" in wm:
  599.         del wm["tpp_toolmode_brusloc"]
  600.  
  601.  
  602. # calculate new snapped location based on start point (sx,sy)
  603. # and current mouse point (mx,my).  These coords appear to be
  604. # in 2D screen coords, with the origin at bottom-left, +x right,
  605. # +y up.
  606. #
  607. def do_snap( sx, sy, mx, my ):
  608.     # compute delta between current mouse position and
  609.     # start position
  610.     dx = mx - sx
  611.     dy = my - sy
  612.     adx = abs(dx)
  613.     ady = abs(dy)
  614.  
  615.     # if delta is "close enough" to the diagonal
  616.     if abs( ady - adx ) < 0.5 * max(adx, ady):
  617.        
  618.         # use a simple algorithm to snap based on horizontal
  619.         # distance (could use vertical distance, or could use
  620.         # radial distance but that would require more calcs).
  621.         if (dx > 0 and dy > 0) or (dx < 0 and dy < 0):
  622.             x = mx
  623.             y = sy + dx
  624.         elif (dx > 0 and dy < 0) or (dx < 0 and dy > 0):
  625.             x = mx
  626.             y = sy - dx
  627.         else:
  628.             x = mx
  629.             y = my
  630.     elif ( adx > ady ):
  631.         # closer to y-axis, snap vertical
  632.         x = mx
  633.         y = sy
  634.     else:
  635.         # closer to x-axis, snap horizontal
  636.         x = sx
  637.         y = my
  638.  
  639.     return (x, y)
  640.  
  641.  
  642.  
  643.  
  644. ##########################################
  645. #                                        #
  646. # Classes                                #
  647. #                                        #
  648. ##########################################
  649.  
  650. class ImageBuffer:
  651.     # based on script by Domino from BlenderArtists
  652.     # licensed GPL v2 or later
  653.     def __init__(self, image):
  654.         self.image = image
  655.         self.x, self.y = self.image.size
  656.         self.buffer = list(self.image.pixels)
  657.  
  658.     def update(self):
  659.         self.image.pixels = self.buffer
  660.  
  661.     def _index(self, x, y):
  662.         if x < 0 or y < 0 or x >= self.x or y >= self.y:
  663.             return None
  664.         return (x + y * self.x) * 4
  665.  
  666.     def set_pixel(self, x, y, colour):
  667.         index = self._index(x, y)
  668.         if index is not None:
  669.             index = int(index)
  670.             self.buffer[index:index + 4] = colour
  671.  
  672.     def get_pixel(self, x, y):
  673.         index = self._index(x, y)
  674.         if index is not None:
  675.             index = int(index)
  676.             return self.buffer[index:index + 4]
  677.         else:
  678.             return None
  679.  
  680.  
  681. # 2d bin packing
  682. class PackTree(object):
  683.     # based on python recipe by S W on ActiveState
  684.     # PSF license, 16 oct 2005. (GPL compatible)
  685.     def __init__(self, area):
  686.         if len(area) == 2:
  687.             area = (0,0,area[0],area[1])
  688.         self.area = area
  689.    
  690.     def get_width(self):
  691.         return self.area[2] - self.area[0]
  692.     width = property(fget=get_width)
  693.    
  694.     def get_height(self):
  695.         return self.area[3] - self.area[1]
  696.     height = property(fget=get_height)
  697.    
  698.     def insert(self, area):
  699.         if hasattr(self, 'child'):
  700.             a = self.child[0].insert(area)
  701.             if a is None:
  702.                 return self.child[1].insert(area)
  703.             else:
  704.                 return a
  705.        
  706.         area = PackTree(area)
  707.         if area.width <= self.width and area.height <= self.height:
  708.             self.child = [None,None]
  709.             self.child[0] = PackTree((self.area[0]+area.width, self.area[1], self.area[2], self.area[1] + area.height))
  710.             self.child[1] = PackTree((self.area[0], self.area[1]+area.height, self.area[2], self.area[3]))
  711.             return PackTree((self.area[0], self.area[1], self.area[0]+area.width, self.area[1]+area.height))
  712.  
  713.  
  714. ##########################################
  715. #                                        #
  716. # Operators                              #
  717. #                                        #
  718. ##########################################
  719.  
  720. class AddDefaultImage(bpy.types.Operator):
  721.     '''Create and assign a new default image to the object'''
  722.     bl_idname = "object.add_default_image"
  723.     bl_label = "Add default image"
  724.    
  725.     @classmethod
  726.     def poll(cls, context):
  727.         return(context.active_object and context.active_object.type=='MESH')
  728.    
  729.     def invoke(self, context, event):
  730.         ob = context.active_object
  731.         mat = bpy.data.materials.new("default")
  732.         tex = bpy.data.textures.new("default", 'IMAGE')
  733.         img = bpy.data.images.new("default", 1024, 1024, alpha=True)
  734.         ts = mat.texture_slots.add()
  735.         tex.image = img
  736.         ts.texture = tex
  737.         ob.data.materials.append(mat)
  738.        
  739.         return {'FINISHED'}
  740.  
  741.  
  742. class AutoMergeUV(bpy.types.Operator):
  743.     '''Have UV Merge enabled by default for merge actions'''
  744.     bl_idname = "paint.auto_merge_uv"
  745.     bl_label = "AutoMerge UV"
  746.    
  747.     def invoke(self, context, event):
  748.         wm = context.window_manager
  749.         if "tpp_automergeuv" not in wm:
  750.             init_props()
  751.         wm["tpp_automergeuv"] = 1 - wm["tpp_automergeuv"]
  752.        
  753.         km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
  754.         for kmi in km.keymap_items:
  755.             if kmi.idname == "mesh.merge":
  756.                 kmi.properties.uvs = wm["tpp_automergeuv"]
  757.        
  758.         return {'FINISHED'}
  759.  
  760.  
  761. class BrushPopup(bpy.types.Operator):
  762.     bl_idname = "paint.brush_popup"
  763.     bl_label = "Brush settings"
  764.     bl_options = {'REGISTER'}
  765.    
  766.     def draw(self, context):
  767.         brush = context.tool_settings.image_paint.brush
  768.        
  769.         # colour buttons
  770.         col = self.layout.column()
  771.         split = col.split(percentage = 0.15)
  772.         split.prop(brush, "color", text="")
  773.         split.scale_y = 1e-6
  774.         col.template_color_wheel(brush, "color", value_slider=True)
  775.        
  776.         # imagepaint tool buttons
  777.         group = col.column(align=True)
  778.         row = group.row(align=True)
  779.         row.prop(brush, "image_tool", expand=True, icon_only=True, emboss=True)
  780.        
  781.         # curve type buttons
  782.         row = group.split(align=True)
  783.         row.operator("brush.curve_preset", icon="SMOOTHCURVE", text="").shape = 'SMOOTH'
  784.         row.operator("brush.curve_preset", icon="SPHERECURVE", text="").shape = 'ROUND'
  785.         row.operator("brush.curve_preset", icon="ROOTCURVE", text="").shape = 'ROOT'
  786.         row.operator("brush.curve_preset", icon="SHARPCURVE", text="").shape = 'SHARP'
  787.         row.operator("brush.curve_preset", icon="LINCURVE", text="").shape = 'LINE'
  788.         row.operator("brush.curve_preset", icon="NOCURVE", text="").shape = 'MAX'
  789.        
  790.         # radius buttons
  791.         col = col.column(align=True)
  792.         row = col.row(align=True)
  793.         row.prop(brush, "size", text="Radius", slider=True)
  794.         row.prop(brush, "use_pressure_size", toggle=True, text="")
  795.         # strength buttons
  796.         row = col.row(align=True)
  797.         row.prop(brush, "strength", text="Strength", slider=True)
  798.         row.prop(brush, "use_pressure_strength", toggle=True, text="")
  799.         # jitter buttons
  800.         row = col.row(align=True)
  801.         row.prop(brush, "jitter", slider=True)
  802.         row.prop(brush, "use_pressure_jitter", toggle=True, text="")
  803.         # spacing buttons
  804.         row = col.row(align=True)
  805.         row.prop(brush, "spacing", slider=True)
  806.         row.prop(brush, "use_space", toggle=True, text="", icon="FILE_TICK")
  807.        
  808.         # alpha and blending mode buttons
  809.         split = col.split(percentage=0.25)
  810.         row = split.row()
  811.         row.active = (brush.blend not in {'ERASE_ALPHA', 'ADD_ALPHA'})
  812.         row.prop(brush, "use_alpha", text="A")
  813.         split.prop(brush, "blend", text="")
  814.    
  815.     def execute(self, context):
  816.         if context.space_data.type == 'IMAGE_EDITOR':
  817.             context.space_data.mode = 'PAINT'
  818.         return context.window_manager.invoke_popup(self, width=125)
  819.  
  820.  
  821. class ChangeSelection(bpy.types.Operator):
  822.     '''Select more or less vertices/edges/faces, connected to the original selection'''
  823.     bl_idname = "paint.change_selection"
  824.     bl_label = "Change selection"
  825.    
  826.     mode = bpy.props.EnumProperty(name="Mode",
  827.         items = (("more", "More", "Select more vertices/edges/faces"),
  828.             ("less", "Less", "Select less vertices/edges/faces")),
  829.         description = "Choose whether the selection should be increased or decreased",
  830.         default = 'more')
  831.    
  832.     @classmethod
  833.     def poll(cls, context):
  834.         return bpy.ops.paint.image_paint.poll()
  835.    
  836.     def invoke(self, context, event):
  837.         bpy.ops.object.mode_set(mode='EDIT')
  838.         if self.mode == 'more':
  839.             bpy.ops.mesh.select_more()
  840.         else: #self.mode == 'less'
  841.             bpy.ops.mesh.select_less()
  842.         bpy.ops.object.mode_set(mode='TEXTURE_PAINT')
  843.        
  844.         return {'FINISHED'}
  845.  
  846.  
  847. class ConsolidateImages(bpy.types.Operator):
  848.     '''Pack all texture images into one image'''
  849.     bl_idname = "image.consolidate"
  850.     bl_label = "Consolidate images"
  851.    
  852.     remap = bpy.props.BoolProperty(default=False)
  853.    
  854.     @classmethod
  855.     def poll(cls, context):
  856.         return(context.active_object or context.selected_objects)
  857.  
  858.     def invoke(self, context, event):
  859.         props = context.window_manager.tpp
  860.         meshes = consolidate_get_input(props.consolidate_input)
  861.         if not meshes:
  862.             self.report({'ERROR'}, "No UV textures found")
  863.             return {'CANCELLED'}
  864.        
  865.         if props.consolidate_split:
  866.             restrict = {"diffuse":["use_map_diffuse", "use_map_color_diffuse", "use_map_alpha", "use_map_translucency"],
  867.                 "shading":["use_map_ambient", "use_map_emit", "use_map_mirror", "use_map_raymir"],
  868.                 "specular":["use_map_specular", "use_map_color_spec", "use_map_hardness"],
  869.                 "geometry":["use_map_normal", "use_map_warp", "use_map_displacement"]}
  870.         else:
  871.             restrict = {"consolidate":False}
  872.        
  873.         all_ts = []
  874.         for restrict_name, restrict_values in restrict.items():
  875.             if context.area:
  876.                 context.area.header_text_set("Collecting images")
  877.             images, textures, texture_slots, ts_found, uv_layers = consolidate_get_images(meshes, restrict_values, all_ts)
  878.             if not images:
  879.                 continue
  880.             all_ts += texture_slots
  881.             if not ts_found:
  882.                 restrict_name = "consolidate"
  883.                 restrict_values = False
  884.             if context.area:
  885.                 context.area.header_text_set("Bin packing images")
  886.             image_map, width, height = consolidate_pack_images(images, props.consolidate_width, props.consolidate_height, props.consolidate_auto_size)
  887.             if context.area:
  888.                 context.area.header_text_set("Rendering composite image")
  889.             resized = consolidate_check_resize(image_map)
  890.             if not resized:
  891.                 # direct copy from source
  892.                 composite_image = consolidate_copy_images(width, height, props.consolidate_alpha, image_map, props.consolidate_filename, props.consolidate_fileformat, restrict_name)
  893.             else:
  894.                 # render to enable resizing
  895.                 composite_image = consolidate_new_image(width, height, props.consolidate_alpha, image_map, props.consolidate_filename, props.consolidate_fileformat, restrict_name)
  896.             if self.remap:
  897.                 if context.area:
  898.                     context.area.header_text_set("Updating textures and UVs")
  899.                 consolidate_update_textures(textures, uv_layers, image_map, composite_image, restrict_name)
  900.             if not ts_found:
  901.                 break
  902.        
  903.         if context.area:
  904.             context.area.header_text_set()        
  905.         return {'FINISHED'}  
  906.  
  907.  
  908. class CycleBlendtype(bpy.types.Operator):
  909.     '''Change the transparency blending mode of the active texture face'''
  910.     bl_idname = "paint.cycle_blendtype"
  911.     bl_label = "Cycle transparency blending mode"
  912.    
  913.     @classmethod
  914.     def poll(cls, context):
  915.         ob = context.active_object
  916.         if not ob or not ob.data:
  917.             return False
  918.         return ob.type == 'MESH'
  919.    
  920.     def invoke(self, context, event):
  921.         object = context.active_object
  922.         mesh = object.data
  923.         old_mode = object.mode
  924.         bpy.ops.object.mode_set(mode="OBJECT")
  925.        
  926.         if context.tool_settings.use_uv_select_sync:
  927.             # use MeshFace selection state
  928.             faces = [[tex, i] for tex in mesh.uv_textures for i, face in enumerate(tex.data) if mesh.faces[i].select]
  929.         else:
  930.             # get MeshTextureFace selection state
  931.             faces = [[tex, i] for tex in mesh.uv_textures for i, face in enumerate(tex.data) if [uv_sel for uv_sel in face.select_uv]==[True,True,True,True]]
  932.         if faces:
  933.             old_type = faces[0][0].data[faces[0][1]].blend_type
  934.             if old_type == 'OPAQUE':
  935.                 new_type = 'ALPHA'
  936.             elif old_type == 'ALPHA':
  937.                 new_type = 'CLIPALPHA'
  938.             else:
  939.                 new_type = 'OPAQUE'
  940.             for [tex, i] in faces:
  941.                 tex.data[i].blend_type = new_type
  942.         bpy.ops.object.mode_set(mode=old_mode)
  943.        
  944.         return {'FINISHED'}
  945.  
  946.  
  947. class DefaultMaterial(bpy.types.Operator):
  948.     '''Add a default dif/spec/normal material to an object'''
  949.     bl_idname = "object.default_material"
  950.     bl_label = "Default material"
  951.    
  952.     @classmethod
  953.     def poll(cls, context):
  954.         object = context.active_object
  955.         if not object or not object.data:
  956.             return False
  957.         return object.type == 'MESH'
  958.    
  959.     def invoke(self, context, event):
  960.         objects = context.selected_objects
  961.         for ob in objects:
  962.             if not ob.data or ob.type != 'MESH':
  963.                 continue
  964.            
  965.         mat = bpy.data.materials.new(ob.name)
  966.        
  967.         # diffuse texture
  968.         tex = bpy.data.textures.new(ob.name+"_D", 'IMAGE')
  969.         ts = mat.texture_slots.add()
  970.         ts.texture_coords = 'UV'
  971.         ts.texture = tex
  972.         # specular texture
  973.         tex = bpy.data.textures.new(ob.name+"_S", 'IMAGE')
  974.         ts = mat.texture_slots.add()
  975.         ts.texture_coords = 'UV'
  976.         ts.use_map_color_diffuse = False
  977.         ts.use_map_specular = True
  978.         ts.texture = tex
  979.         # normal texture
  980.         tex = bpy.data.textures.new(ob.name+"_N", 'IMAGE')
  981.         tex.use_normal_map = True
  982.         ts = mat.texture_slots.add()
  983.         ts.texture_coords = 'UV'
  984.         ts.use_map_color_diffuse = False
  985.         ts.use_map_normal = True
  986.         ts.texture = tex
  987.        
  988.         ob.data.materials.append(mat)
  989.        
  990.         return {'FINISHED'}
  991.    
  992.  
  993.  
  994. class DisplayToolMode(bpy.types.Operator):
  995.     '''Display paint tool and blend mode in the 3d-view'''
  996.     bl_idname = "paint.display_tool_mode"
  997.     bl_label = "Tool + Mode"
  998.    
  999.     _handle = None
  1000.     _timer = None
  1001.    
  1002.     @classmethod
  1003.     def poll(cls, context):
  1004.         return context.mode == 'PAINT_TEXTURE'
  1005.    
  1006.     def modal(self, context, event):
  1007.         if context.window_manager.tpp.toolmode_enabled == -1:
  1008.             context.window_manager.event_timer_remove(self._timer)
  1009.             context.region.callback_remove(self._handle)
  1010.             context.window_manager.tpp.toolmode_enabled = 0
  1011.             if context.area:
  1012.                 context.area.tag_redraw()
  1013.             return {'CANCELLED'}
  1014.        
  1015.         if context.area:
  1016.             context.area.tag_redraw()
  1017.        
  1018.         if event.type == 'TIMER':
  1019.             brush = context.tool_settings.image_paint.brush
  1020.             if brush.name != context.window_manager.tpp.toolmode_tool:
  1021.                 context.window_manager.tpp.toolmode_tool = brush.name
  1022.                 context.window_manager["tpp_toolmode_time"] = time.time()
  1023.             if brush.blend != context.window_manager.tpp.toolmode_mode:
  1024.                 context.window_manager.tpp.toolmode_mode = brush.blend
  1025.                 context.window_manager["tpp_toolmode_time"] = time.time()
  1026.             if time.time() - context.window_manager["tpp_toolmode_time"] < 1:
  1027.                 x = event.mouse_region_x
  1028.                 y = event.mouse_region_y + brush.size + 5
  1029.                 context.window_manager["tpp_toolmode_brushloc"] = (x, y)
  1030.        
  1031.         return {'PASS_THROUGH'}
  1032.    
  1033.     def cancel(self, context):
  1034.         try:
  1035.             context.window_manager.event_timer_remove(self._timer)
  1036.             context.region.callback_remove(self._handle)
  1037.             context.window_manager.tpp.toolmode_enabled = 0
  1038.         except:
  1039.             pass
  1040.         return {'CANCELLED'}
  1041.    
  1042.     def invoke(self, context, event):
  1043.         if context.window_manager.tpp.toolmode_enabled == 0:
  1044.             brush = context.tool_settings.image_paint.brush
  1045.             context.window_manager.tpp.toolmode_enabled = 1
  1046.             context.window_manager["tpp_toolmode_time"] = time.time()
  1047.             context.window_manager.tpp.toolmode_tool = brush.name
  1048.             context.window_manager.tpp.toolmode_mode = brush.blend
  1049.             context.window_manager.modal_handler_add(self)
  1050.             self._handle = context.region.callback_add(toolmode_draw_callback,
  1051.                 (self, context), 'POST_PIXEL')
  1052.             self._timer = context.window_manager.event_timer_add(0.02, context.window)
  1053.         else:
  1054.             context.window_manager.tpp.toolmode_enabled = -1
  1055.             return {'CANCELLED'}
  1056.        
  1057.         return {'RUNNING_MODAL'}
  1058.  
  1059.  
  1060.  
  1061. class GridTexture(bpy.types.Operator):
  1062.     '''Toggle between current texture and UV / Colour grids'''
  1063.     bl_idname = "paint.grid_texture"
  1064.     bl_label = "Grid texture"
  1065.    
  1066.     @classmethod
  1067.     def poll(cls, context):
  1068.         return bpy.ops.paint.image_paint.poll()
  1069.    
  1070.     def invoke(self, context, event):
  1071.         objects = bpy.context.selected_objects
  1072.         meshes = [object.data for object in objects if object.type == 'MESH']
  1073.         if not meshes:
  1074.             self.report({'INFO'}, "Couldn't locate meshes to operate on")
  1075.             return {'CANCELLED'}
  1076.        
  1077.         tex_image = []
  1078.         for mesh in meshes:
  1079.             for mat in mesh.materials:
  1080.                 for tex in [ts.texture for ts in mat.texture_slots if ts and ts.texture.type=='IMAGE' and ts.texture.image]:
  1081.                     tex_image.append([tex.name, tex.image.name])
  1082.         if not tex_image:
  1083.             self.report({'INFO'}, "Couldn't locate textures to operate on")
  1084.             return {'CANCELLED'}
  1085.        
  1086.         first_image = bpy.data.images[tex_image[0][1]]
  1087.         if "grid_texture_mode" in first_image:
  1088.             mode = first_image["grid_texture_mode"]
  1089.         else:
  1090.             mode = 1
  1091.        
  1092.         if mode == 1:
  1093.             # original textures, change to new UV grid
  1094.             width = max([bpy.data.images[image].size[0] for tex, image in tex_image])
  1095.             height = max([bpy.data.images[image].size[1] for tex, image in tex_image])
  1096.             new_image = bpy.data.images.new("temp_grid", width=width, height=height)
  1097.             new_image.generated_type = 'UV_GRID'
  1098.             new_image["grid_texture"] = tex_image
  1099.             new_image["grid_texture_mode"] = 2
  1100.             for tex, image in tex_image:
  1101.                 bpy.data.textures[tex].image = new_image
  1102.         elif mode == 2:
  1103.             # change from UV grid to Colour grid
  1104.             first_image.generated_type = 'COLOR_GRID'
  1105.             first_image["grid_texture_mode"] = 3
  1106.         elif mode == 3:
  1107.             # change from Colour grid back to original textures
  1108.             if "grid_texture" not in first_image:
  1109.                 first_image["grid_texture_mode"] = 1
  1110.                 self.report({'ERROR'}, "Couldn't retrieve original images")
  1111.                 return {'FINISHED'}
  1112.             tex_image = first_image["grid_texture"]
  1113.             for tex, image in tex_image:
  1114.                 if tex in bpy.data.textures and image in bpy.data.images:
  1115.                     bpy.data.textures[tex].image = bpy.data.images[image]
  1116.             bpy.data.images.remove(first_image)
  1117.        
  1118.         return {'FINISHED'}
  1119.  
  1120.  
  1121. class MassLinkAppend(bpy.types.Operator, ImportHelper):
  1122.     '''Import objects from multiple blend-files at the same time'''
  1123.     bl_idname = "wm.mass_link_append"
  1124.     bl_label = "Mass Link/Append"
  1125.     bl_options = {'REGISTER', 'UNDO'}
  1126.    
  1127.     active_layer = bpy.props.BoolProperty(name="Active Layer",
  1128.         default=True,
  1129.         description="Put the linked objects on the active layer")
  1130.     autoselect = bpy.props.BoolProperty(name="Select",
  1131.         default=True,
  1132.         description="Select the linked objects")
  1133.     instance_groups = bpy.props.BoolProperty(name="Instance Groups",
  1134.         default=False,
  1135.         description="Create instances for each group as a DupliGroup")
  1136.     link = bpy.props.BoolProperty(name="Link",
  1137.         default=False,
  1138.         description="Link the objects or datablocks rather than appending")
  1139.     relative_path = bpy.props.BoolProperty(name="Relative Path",
  1140.         default=True,
  1141.         description="Select the file relative to the blend file")
  1142.    
  1143.     def execute(self, context):
  1144.         directory, filename = os.path.split(bpy.path.abspath(self.filepath))
  1145.         files = []
  1146.        
  1147.         # find all blend-files in the given directory
  1148.         for root, dirs, filenames in os.walk(directory):
  1149.             for file in filenames:
  1150.                 if file.endswith(".blend"):
  1151.                     files.append([root+os.sep, file])
  1152.             break # don't search in subdirectories
  1153.        
  1154.         # append / link objects
  1155.         old_selection = context.selected_objects
  1156.         new_selection = []
  1157.         print("_______ Texture Paint Plus _______")
  1158.         print("You can safely ignore the line(s) below")
  1159.         for directory, filename in files:
  1160.             # get object names
  1161.             with bpy.data.libraries.load(directory + filename) as (append_lib, current_lib):
  1162.                 ob_names = append_lib.objects
  1163.             for name in ob_names:
  1164.                 append_libs = [{"name":name} for name in ob_names]
  1165.             # appending / linking
  1166.             bpy.ops.wm.link_append(filepath=os.sep+filename+os.sep+"Object"+os.sep,
  1167.                 filename=name, directory=directory+filename+os.sep+"Object"+os.sep,
  1168.                 link=self.link, autoselect=True, active_layer=self.active_layer,
  1169.                 relative_path=self.relative_path, instance_groups=self.instance_groups,
  1170.                 files=append_libs)
  1171.             if not self.link:
  1172.                 bpy.ops.object.mode_set(mode='OBJECT')
  1173.                 bpy.ops.object.make_local()
  1174.                 bpy.ops.object.make_local(type='SELECTED_OBJECTS_DATA')
  1175.             new_selection += context.selected_objects
  1176.         print("__________________________________")
  1177.         bpy.ops.object.select_all(action='DESELECT')
  1178.         if self.autoselect:
  1179.             for ob in new_selection:
  1180.                 ob.select = True
  1181.         else:
  1182.             for ob in old_selection:
  1183.                 ob.select = True
  1184.        
  1185.         return {'FINISHED'}
  1186.  
  1187.  
  1188. class OriginSet(bpy.types.Operator):
  1189.     '''Set origin while in editmode'''
  1190.     bl_idname = "mesh.origin_set"
  1191.     bl_label = "Set Origin"
  1192.    
  1193.     type = bpy.props.EnumProperty(name="Type",
  1194.         items = (("GEOMETRY_ORIGIN", "Geometry to Origin", "Move object geometry to object origin"),
  1195.             ("ORIGIN_GEOMETRY", "Origin to Geometry", "Move object origin to center of object geometry"),
  1196.             ("ORIGIN_CURSOR", "Origin to 3D Cursor", "Move object origin to position of the 3d cursor")),
  1197.         default = 'GEOMETRY_ORIGIN')
  1198.     center = bpy.props.EnumProperty(name="Center",
  1199.         items=(("MEDIAN", "Median Center", ""),
  1200.             ("BOUNDS", "Bounds Center", "")),
  1201.         default = 'MEDIAN')
  1202.    
  1203.     @classmethod
  1204.     def poll(cls, context):
  1205.         ob = context.active_object
  1206.         if not ob or not ob.data:
  1207.             return False
  1208.         return ob.type == 'MESH'
  1209.    
  1210.     def execute(self, context):
  1211.         object = context.active_object
  1212.         mesh = object.data
  1213.         old_mode = object.mode
  1214.         bpy.ops.object.mode_set(mode="OBJECT")
  1215.         bpy.ops.object.origin_set(type=self.type, center=self.center)
  1216.         bpy.ops.object.mode_set(mode=old_mode)
  1217.        
  1218.         return {'FINISHED'}
  1219.  
  1220.  
  1221. class ReloadImage(bpy.types.Operator):
  1222.     '''Reload image displayed in image-editor'''
  1223.     bl_idname = "paint.reload_image"
  1224.     bl_label = "Reload image"
  1225.    
  1226.     def invoke(self, context, event):
  1227.         images = get_images_in_editors(context)
  1228.         for img in images:
  1229.             img.reload()
  1230.        
  1231.         # make the changes immediately visible in 3d-views
  1232.         # image editor updating is handled in get_images_in_editors()
  1233.         for area in context.screen.areas:
  1234.             if area.type == 'VIEW_3D':
  1235.                 area.tag_redraw()
  1236.        
  1237.         return{'FINISHED'}
  1238.  
  1239.  
  1240. class ReloadImages(bpy.types.Operator):
  1241.     '''Reload all images'''
  1242.     bl_idname = "paint.reload_images"
  1243.     bl_label = "Reload all images"
  1244.    
  1245.     def invoke(self, context, event):
  1246.         reloaded = [0, 0]
  1247.         for img in bpy.data.images:
  1248.             img.reload()
  1249.        
  1250.         # make the changes immediately visible in image editors and 3d-views
  1251.         for area in context.screen.areas:
  1252.             if area.type == 'IMAGE_EDITOR' or area.type == 'VIEW_3D':
  1253.                 area.tag_redraw()
  1254.        
  1255.         return {'FINISHED'}
  1256.  
  1257.  
  1258. class SampleColor(bpy.types.Operator):
  1259.     '''Sample color'''
  1260.     bl_idname = "paint.sample_color_custom"
  1261.     bl_label = "Sample color"
  1262.    
  1263.     @classmethod
  1264.     def poll(cls, context):
  1265.         return bpy.ops.paint.image_paint.poll()
  1266.    
  1267.     def invoke(self, context, event):
  1268.         mesh = context.active_object.data
  1269.         paint_mask = mesh.use_paint_mask
  1270.         mesh.use_paint_mask = False
  1271.         bpy.ops.paint.sample_color('INVOKE_REGION_WIN')
  1272.         mesh.use_paint_mask = paint_mask
  1273.        
  1274.         return {'FINISHED'}
  1275.  
  1276.  
  1277. class SaveImage(bpy.types.Operator):
  1278.     '''Save image displayed in image-editor'''
  1279.     bl_idname = "paint.save_image"
  1280.     bl_label = "Save image"
  1281.    
  1282.     def invoke(self, context, event):
  1283.         images = get_images_in_editors(context)
  1284.         for img in images:
  1285.             img.save()
  1286.  
  1287.         return{'FINISHED'}
  1288.  
  1289.  
  1290. class SaveImages(bpy.types.Operator):
  1291.     '''Save all images'''
  1292.     bl_idname = "wm.save_images"
  1293.     bl_label = "Save all images"
  1294.    
  1295.     def invoke(self, context, event):
  1296.         correct = 0
  1297.         for img in bpy.data.images:
  1298.             try:
  1299.                 img.save()
  1300.                 correct += 1
  1301.             except:
  1302.                 # some images don't have a source path (e.g. render result)
  1303.                 pass
  1304.        
  1305.         self.report({'INFO'}, "Saved " + str(correct) + " images")
  1306.        
  1307.         return {'FINISHED'}
  1308.  
  1309.  
  1310.  
  1311. class StraightLine(bpy.types.Operator):
  1312.     '''Paint a straight line'''
  1313.     bl_idname = "paint.straight_line"
  1314.     bl_label = "Straight line"
  1315.     time = 0
  1316.     stroke = []
  1317.  
  1318.     def to_stroke_elem(self, x, y):
  1319.        
  1320.         elem = {"name":"",
  1321.                 "pen_flip":False,
  1322.                 "is_start":False,
  1323.                 "location":(0,0,0),
  1324.                 "mouse":(x, y),
  1325.                 "pressure":1,
  1326.                 "time": self.time
  1327.                }
  1328.                
  1329.         return elem
  1330.    
  1331.     @classmethod
  1332.     def poll(cls, context):
  1333.         return bpy.ops.paint.image_paint.poll()
  1334.    
  1335.     def modal(self, context, event):
  1336.         if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
  1337.             #context.region.callback_remove(self.handle)
  1338.             bpy.types.SpaceView3D.draw_handler_remove(self._v3dhandle, 'WINDOW')
  1339.             bpy.types.SpaceImageEditor.draw_handler_remove(self._imghandle, 'WINDOW')
  1340.             #Hrm, since the paint operator paints only 2 dots with 2 stroke elems
  1341.             #need to interpolate the rest to be between 1st and last, BAD...
  1342.             s = [self.stroke[0], self.stroke[-1]]
  1343.             start_x = s[0]["mouse"][0]
  1344.             start_y = s[0]["mouse"][1]
  1345.             length_x = s[1]["mouse"][0] - start_x
  1346.             length_y = s[1]["mouse"][1] - start_y
  1347.             length = int(math.sqrt( length_x * length_x + length_y * length_y ) / 2.0)
  1348.             stroke = []
  1349.             self.time = 0.0
  1350.             #print( "INFO>>> processing " + str(length) + " units length..." )
  1351.             if (length <= 0):
  1352.                 self.report({'INFO'}, "Mouse movement too short to process")
  1353.                 return {'FINISHED'}
  1354.                
  1355.             # the following is an attempt to reduce the "spottiness" caused by
  1356.             # multiple brush placements... by brushing the path slowly both ways.
  1357.             #
  1358.             for i in range(length):
  1359.                 r = i / length;
  1360.                 x = start_x + length_x * r;
  1361.                 y = start_y + length_y * r;
  1362.                 self.time += 1.0
  1363.                 stroke.append( self.to_stroke_elem( x, y ) )
  1364.             for i in range(length):
  1365.                 r = (length - i - 1) / length
  1366.                 x = start_x + length_x * r;
  1367.                 y = start_y + length_y * r;
  1368.                 self.time += 1.0
  1369.                 stroke.append( self.to_stroke_elem( x, y ) )
  1370.             stroke[0]["is_start"] = True
  1371.             #print( "INFO>>> painting stroke with " + str(len(stroke)) + " elements..." )
  1372.             bpy.ops.paint.image_paint(stroke=stroke)
  1373.             self.time = 0
  1374.             self.stroke = []
  1375.             context.window_manager.tpp.line_last = True
  1376.             context.window_manager.tpp.line_x = s[1]["mouse"][0]
  1377.             context.window_manager.tpp.line_y = s[1]["mouse"][1]
  1378.             return {'FINISHED'}
  1379.        
  1380.         elif event.type == 'MOUSEMOVE':
  1381.             if event.ctrl:
  1382.                 x, y = do_snap( self.stroke[0]["mouse"][0],
  1383.                                 self.stroke[0]["mouse"][1],
  1384.                                 event.mouse_region_x,
  1385.                                 event.mouse_region_y )
  1386.             else:
  1387.                 x = event.mouse_region_x
  1388.                 y = event.mouse_region_y
  1389.  
  1390.             self.stroke[-1]["mouse"] = (x, y)
  1391.        
  1392.         if context.area:
  1393.             context.area.tag_redraw()
  1394.        
  1395.         return {'RUNNING_MODAL'}
  1396.    
  1397.     def invoke(self, context, event):
  1398.         self.stroke = []
  1399.         if (event.alt and context.window_manager.tpp.line_last):
  1400.             elem = self.to_stroke_elem( context.window_manager.tpp.line_x,
  1401.                                         context.window_manager.tpp.line_y )
  1402.         else:
  1403.             context.window_manager.tpp.line_last = False
  1404.             elem = self.to_stroke_elem(event.mouse_region_x,
  1405.                                        event.mouse_region_y)
  1406.         elem["is_start"] = True
  1407.         self.stroke.append(copy.deepcopy(elem))
  1408.         elem["is_start"] = False
  1409.         self.stroke.append(elem)
  1410.         context.window_manager.modal_handler_add(self)
  1411.         # this was the "old" (pre ~2.66) way...
  1412.         #self.handle = context.region.callback_add(draw_callback,
  1413.         #    (self, context), 'POST_PIXEL')
  1414.         args = (self, context)
  1415.         self._v3dhandle = bpy.types.SpaceView3D.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_PIXEL')
  1416.         self._imghandle = bpy.types.SpaceImageEditor.draw_handler_add(draw_callback, args, 'WINDOW', 'POST_PIXEL')
  1417.         return {'RUNNING_MODAL'}
  1418.  
  1419.  
  1420. class SyncSelection(bpy.types.Operator):
  1421.     '''Sync selection from uv-editor to 3d-view'''
  1422.     bl_idname = "uv.sync_selection"
  1423.     bl_label = "Sync selection"
  1424.    
  1425.     _timer = None
  1426.     _selection_3d = []
  1427.     handle1 = None
  1428.     handle2 = None
  1429.     handle3 = None
  1430.     area = None
  1431.     region = None
  1432.     overlay_vertices = []
  1433.     overlay_edges = []
  1434.     overlay_faces = []
  1435.     position_vertices = []
  1436.     position_edges = []
  1437.     position_faces = []
  1438.     position2_vertices = []
  1439.     position2_edges = []
  1440.     position2_edges = []
  1441.    
  1442.     @classmethod
  1443.     def poll(cls, context):
  1444.         return(context.active_object and context.active_object.mode=='EDIT')
  1445.    
  1446.     def modal(self, context, event):
  1447.         if self.area:
  1448.             self.area.tag_redraw()
  1449.         if context.area:
  1450.             context.area.tag_redraw()
  1451.        
  1452.         if context.window_manager.tpp.sync_enabled == -1:
  1453.             self.region.callback_remove(self.handle1)
  1454.             self.region.callback_remove(self.handle2)
  1455.             context.region.callback_remove(self.handle3)
  1456.             self.area = None
  1457.             self.region = None
  1458.             context.window_manager.tpp.sync_enabled = 0
  1459.             return {"CANCELLED"}
  1460.        
  1461.         return {'PASS_THROUGH'}
  1462.    
  1463.     def invoke(self, context, event):
  1464.         if context.window_manager.tpp.sync_enabled < 1:
  1465.             for area in context.screen.areas:
  1466.                 if area.type == 'VIEW_3D':
  1467.                     self.area = area
  1468.                     for region in area.regions:
  1469.                         if region.type == 'WINDOW':
  1470.                             self.region = region
  1471.                             context.window_manager.tpp.sync_enabled = 1
  1472.                            
  1473.                             # getting overlay selection
  1474.                             old_sync = context.tool_settings.use_uv_select_sync
  1475.                             old_select_mode = [x for x in context.tool_settings.mesh_select_mode]
  1476.                             context.tool_settings.mesh_select_mode = [True, False, False]
  1477.                             bpy.ops.object.mode_set(mode='OBJECT')
  1478.                             mesh = context.active_object.data
  1479.                             self._selection_3d = [v.index for v in mesh.vertices if v.select]
  1480.                             tfl = mesh.uv_textures.active
  1481.                             selected = []
  1482.                             for mface, tface in zip(mesh.faces, tfl.data):
  1483.                                 selected += [mface.vertices[i] for i, x in enumerate(tface.select_uv) if x]
  1484.                             bpy.ops.object.mode_set(mode='EDIT')
  1485.                             bpy.ops.mesh.select_all(action='DESELECT')
  1486.                             bpy.ops.object.mode_set(mode='OBJECT')
  1487.                             context.tool_settings.use_uv_select_sync = True
  1488.                             for v in selected:
  1489.                                 mesh.vertices[v].select = True
  1490.                            
  1491.                             bpy.ops.object.mode_set(mode='EDIT')
  1492.                             bpy.ops.object.mode_set(mode='OBJECT')
  1493.                            
  1494.                             # indices for overlay in 3d-view
  1495.                             self.overlay_vertices = [vertex.index for vertex in mesh.vertices if vertex.select]
  1496.                             self.overlay_edges = [edge.index for edge in mesh.edges if edge.select]
  1497.                             self.overlay_faces = [face.index for face in mesh.faces if face.select]
  1498.                            
  1499.                             # overlay positions for image editor
  1500.                             dict_vertex_pos = dict([[i, []] for i in range(len(mesh.vertices))])
  1501.                             tfl = mesh.uv_textures.active
  1502.                             for mface, tface in zip(mesh.faces, tfl.data):
  1503.                                 for i, vert in enumerate(mface.vertices):
  1504.                                     dict_vertex_pos[vert].append([co for co in tface.uv[i]])
  1505.                            
  1506.                             self.position2_vertices = []
  1507.                             for v in self.overlay_vertices:
  1508.                                 for pos in dict_vertex_pos[v]:
  1509.                                     self.position2_vertices.append(pos)
  1510.                            
  1511.                             # set everything back to original state
  1512.                             bpy.ops.object.mode_set(mode='EDIT')
  1513.                             context.tool_settings.use_uv_select_sync = old_sync
  1514.                             bpy.ops.mesh.select_all(action='DESELECT')
  1515.                             bpy.ops.object.mode_set(mode='OBJECT')
  1516.                             for v in self._selection_3d:
  1517.                                 mesh.vertices[v].select = True
  1518.                             bpy.ops.object.mode_set(mode='EDIT')
  1519.                             context.tool_settings.mesh_select_mode = old_select_mode
  1520.                            
  1521.                            
  1522.                             # 3d view callbacks
  1523.                             context.window_manager.modal_handler_add(self)
  1524.                             self.handle1 = region.callback_add(sync_calc_callback,
  1525.                                 (self, context, area, region), "POST_VIEW")
  1526.                             self.handle2 = region.callback_add(sync_draw_callback,
  1527.                                 (self, context), "POST_PIXEL")
  1528.                            
  1529.                             # image editor callback
  1530.                             self.handle3 = context.region.callback_add(sync_draw_callback2,
  1531.                                 (self, context), "POST_VIEW")
  1532.                            
  1533.                             break
  1534.                     break
  1535.         else:
  1536.             context.window_manager.tpp.sync_enabled = -1
  1537.        
  1538.         return {'RUNNING_MODAL'}
  1539.  
  1540.  
  1541. class ToggleAddMultiply(bpy.types.Operator):
  1542.     '''Toggle between Add and Multiply blend modes'''
  1543.     bl_idname = "paint.toggle_add_multiply"
  1544.     bl_label = "Toggle add/multiply"
  1545.    
  1546.     @classmethod
  1547.     def poll(cls, context):
  1548.         return bpy.ops.paint.image_paint.poll()
  1549.    
  1550.     def invoke(self, context, event):
  1551.         brush = context.tool_settings.image_paint.brush
  1552.         if brush.blend != 'ADD':
  1553.             brush.blend = 'ADD'
  1554.         else:
  1555.             brush.blend = 'MUL'
  1556.        
  1557.         return {'FINISHED'}
  1558.  
  1559.  
  1560. class ToggleAlphaMode(bpy.types.Operator):
  1561.     '''Toggle between Add Alpha and Erase Alpha blend modes'''
  1562.     bl_idname = "paint.toggle_alpha_mode"
  1563.     bl_label = "Toggle alpha mode"
  1564.    
  1565.     @classmethod
  1566.     def poll(cls, context):
  1567.         return bpy.ops.paint.image_paint.poll()
  1568.    
  1569.     def invoke(self, context, event):
  1570.         brush = context.tool_settings.image_paint.brush
  1571.         if brush.blend != 'ERASE_ALPHA':
  1572.             brush.blend = 'ERASE_ALPHA'
  1573.         else:
  1574.             brush.blend = 'ADD_ALPHA'
  1575.        
  1576.         return {'FINISHED'}
  1577.  
  1578.  
  1579. class ToggleImagePaint(bpy.types.Operator):
  1580.     '''Toggle image painting in the UV/Image editor'''
  1581.     bl_idname = "paint.toggle_image_paint"
  1582.     bl_label = "Image Painting"
  1583.    
  1584.     @classmethod
  1585.     def poll(cls, context):
  1586.         return(context.space_data.type == 'IMAGE_EDITOR')
  1587.    
  1588.     def invoke(self, context, event):
  1589.         if (context.space_data.mode == 'VIEW'):
  1590.             context.space_data.mode = 'PAINT'
  1591.         elif (context.space_data.mode == 'PAINT'):
  1592.             context.space_data.mode = 'MASK'
  1593.         elif (context.space_data.mode == 'MASK'):
  1594.             context.space_data.mode = 'VIEW'
  1595.        
  1596.         return {'FINISHED'}
  1597.  
  1598. class ToggleUVSelectSync(bpy.types.Operator):
  1599.     '''Toggle use_uv_select_sync in the UV editor'''
  1600.     bl_idname = "uv.toggle_uv_select_sync"
  1601.     bl_label = "UV Select Sync"
  1602.    
  1603.     @classmethod
  1604.     def poll(cls, context):
  1605.         return(context.space_data.type == 'IMAGE_EDITOR')
  1606.    
  1607.     def invoke(self, context, event):
  1608.         context.tool_settings.use_uv_select_sync = not context.tool_settings.use_uv_select_sync
  1609.        
  1610.         return {'FINISHED'}
  1611.  
  1612.  
  1613. ##########################################
  1614. #                                        #
  1615. # User interface                         #
  1616. #                                        #
  1617. ##########################################
  1618.  
  1619. # panel with consolidate image options
  1620. class VIEW3D_PT_tools_consolidate(bpy.types.Panel):
  1621.     bl_label = "Consolidate Images"
  1622.     bl_space_type = 'VIEW_3D'
  1623.     bl_region_type = 'TOOLS'
  1624.    
  1625.     def draw(self, context):
  1626.         props = context.window_manager.tpp
  1627.         layout = self.layout
  1628.        
  1629.         col = layout.column(align=True)
  1630.         col.prop(props, "consolidate_filename", text="")
  1631.         col.prop(props, "consolidate_fileformat", text="")
  1632.         col = layout.column(align=True)
  1633.         col.prop(props, "consolidate_input", text="")
  1634.         row = col.row()
  1635.         col_left = row.column(align=True)
  1636.         col_left.active = not props.consolidate_auto_size
  1637.         col_right = row.column()
  1638.         col_left.prop(props, "consolidate_width")
  1639.         col_left.prop(props, "consolidate_height")
  1640.         col_right.prop(props, "consolidate_auto_size")
  1641.         col_right.prop(props, "consolidate_alpha")
  1642.         layout.prop(props, "consolidate_split")
  1643.         col = layout.column(align=True)
  1644.         col.operator("image.consolidate", text="Consolidate")
  1645.         col.operator("image.consolidate", text="Consolidate + remap").remap = True
  1646.  
  1647.  
  1648. # property group containing all properties of the add-on
  1649. class TexturePaintPlusProps(bpy.types.PropertyGroup):
  1650.     consolidate_alpha = bpy.props.BoolProperty(name = "Alpha",
  1651.         description = "Determines if consolidated image has an alpha channel",
  1652.         default = True)
  1653.     consolidate_auto_size = bpy.props.BoolProperty(name = "Auto",
  1654.         description = "Automatically determine size of consolidated image, no resizing is done",
  1655.         default = False)
  1656.     consolidate_fileformat = bpy.props.EnumProperty(name = "Fileformat",
  1657.         items = (("BMP", "BMP", "Output image in bitmap format"),
  1658.             ("IRIS", "Iris", "Output image in (old!) SGI IRIS format"),
  1659.             ("PNG", "PNG", "Output image in PNG format"),
  1660.             ("JPEG", "JPEG", "Output image in JPEG format"),
  1661.             ("TARGA", "Targa", "Output image in Targa format"),
  1662.             ("TARGA_RAW", "Targa Raw", "Output image in uncompressed Targa format"),
  1663.             ("CINEON", "Cineon", "Output image in Cineon format"),
  1664.             ("DPX", "DPX", "Output image in DPX format"),
  1665.             ("MULTILAYER", "MultiLayer", "Output image in multilayer OpenEXR format"),
  1666.             ("OPEN_EXR", "OpenEXR", "Output image in OpenEXR format"),
  1667.             ("HDR", "Radiance HDR", "Output image in Radiance HDR format"),
  1668.             ("TIFF", "TIFF", "Output image in TIFF format")),
  1669.         description = "File format to save the rendered image as",
  1670.         default = 'TARGA')
  1671.     consolidate_filename = bpy.props.StringProperty(name = "Filename",
  1672.         description = "Location where to store the consolidated image",
  1673.         default = "//consolidate",
  1674.         subtype = 'FILE_PATH')
  1675.     consolidate_height = bpy.props.IntProperty(name = "Y",
  1676.         description = "Image height",
  1677.         min = 1,
  1678.         default = 1024)
  1679.     consolidate_input = bpy.props.EnumProperty(name = "Input",
  1680.         items = (("active", "Only active object", "Only combine the images of the active object"),
  1681.             ("selected", "All selected objects", "Combine the images of all selected objects")),
  1682.         description = "Objects of which the images will be consolidated into a single image",
  1683.         default = 'selected')
  1684.     consolidate_split = bpy.props.BoolProperty(name = "Split by type",
  1685.         description = "Consolidate Diffuse, Shading, Specular and Geometry influences into different images",
  1686.         default = True)
  1687.     consolidate_width = bpy.props.IntProperty(name = "X",
  1688.         description = "Image width",
  1689.         min = 1,
  1690.         default = 1024)
  1691.     sync_enabled = bpy.props.IntProperty(name = "Enabled",
  1692.         description = "internal use",
  1693.         default = 0)
  1694.     toolmode_enabled = bpy.props.IntProperty(name = "Enabled",
  1695.         description = "internal use",
  1696.         default = 0)
  1697.     toolmode_mode = bpy.props.StringProperty(name = "Mode",
  1698.         description = "internal use",
  1699.         default = "")
  1700.     toolmode_tool = bpy.props.StringProperty(name = "Tool",
  1701.         description = "internal use",
  1702.         default = "")
  1703.     line_last = bpy.props.BoolProperty(name = "Last_f",
  1704.                                                description = "Last position valid",
  1705.                                                default = False)
  1706.     line_x = bpy.props.IntProperty(name = "Last_x",
  1707.                                    description = "Last position X",
  1708.                                    default = 0)
  1709.     line_y = bpy.props.IntProperty(name = "Last_y",
  1710.                                    description = "Last position y",
  1711.                                    default = 0)
  1712.  
  1713.  
  1714.  
  1715. classes = [AddDefaultImage,
  1716.     AutoMergeUV,
  1717.     BrushPopup,
  1718.     ChangeSelection,
  1719.     ConsolidateImages,
  1720. #    CycleBlendtype,
  1721.     DefaultMaterial,
  1722.     DisplayToolMode,
  1723.     GridTexture,
  1724.     MassLinkAppend,
  1725.     OriginSet,
  1726.     ReloadImage,
  1727.     ReloadImages,
  1728.     SampleColor,
  1729.     SaveImage,
  1730.     SaveImages,
  1731.     StraightLine,
  1732.     SyncSelection,
  1733.     ToggleAddMultiply,
  1734.     ToggleAlphaMode,
  1735.     ToggleImagePaint,
  1736.     ToggleUVSelectSync,
  1737.     VIEW3D_PT_tools_consolidate,
  1738.     TexturePaintPlusProps]
  1739.  
  1740.  
  1741. def menu_func(self, context):
  1742.     layout = self.layout
  1743.     wm = context.window_manager
  1744.     if "tpp_automergeuv" not in wm:
  1745.         automergeuv_enabled = False
  1746.     else:
  1747.         automergeuv_enabled = wm["tpp_automergeuv"]
  1748.    
  1749.     if automergeuv_enabled:
  1750.         layout.operator("paint.auto_merge_uv", icon="CHECKBOX_HLT")
  1751.     else:
  1752.         layout.operator("paint.auto_merge_uv", icon="CHECKBOX_DEHLT")
  1753.  
  1754.  
  1755. def menu_mesh_select_mode(self, context):
  1756.     layout = self.layout
  1757.     layout.separator()
  1758.    
  1759.     prop = layout.operator("wm.context_set_value", text="Vertex + Edge", icon='EDITMODE_HLT')
  1760.     prop.value = "(True, True, False)"
  1761.     prop.data_path = "tool_settings.mesh_select_mode"
  1762.    
  1763.     prop = layout.operator("wm.context_set_value", text="Vertex + Face", icon='ORTHO')
  1764.     prop.value = "(True, False, True)"
  1765.     prop.data_path = "tool_settings.mesh_select_mode"
  1766.    
  1767.     prop = layout.operator("wm.context_set_value", text="Edge + Face", icon='SNAP_FACE')
  1768.     prop.value = "(False, True, True)"
  1769.     prop.data_path = "tool_settings.mesh_select_mode"
  1770.    
  1771.     layout.separator()
  1772.    
  1773.     prop = layout.operator("wm.context_set_value", text="All", icon='OBJECT_DATAMODE')
  1774.     prop.value = "(True, True, True)"
  1775.     prop.data_path = "tool_settings.mesh_select_mode"
  1776.  
  1777.  
  1778. def menu_snap(self, context):
  1779.     layout = self.layout
  1780.     layout.separator()
  1781.    
  1782.     layout.operator("mesh.origin_set", text="Geometry to Origin")
  1783.     layout.operator("mesh.origin_set", text="Origin to Geometry").type = 'ORIGIN_GEOMETRY'
  1784.     layout.operator("mesh.origin_set", text="Origin to 3D Cursor").type = 'ORIGIN_CURSOR'
  1785.  
  1786.  
  1787. def register():
  1788.     import bpy
  1789.     # register classes
  1790.     init_props()
  1791.     for c in classes:
  1792.         bpy.utils.register_class(c)
  1793.     bpy.types.WindowManager.tpp = bpy.props.PointerProperty(\
  1794.         type = TexturePaintPlusProps)
  1795.    
  1796.     # add Image Paint keymap entries
  1797.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Image Paint']
  1798.     kmi = km.keymap_items.new("paint.toggle_alpha_mode", 'A', 'PRESS')
  1799. #    kmi = km.keymap_items.new("paint.cycle_blendtype", 'A', 'PRESS',
  1800. #        ctrl=True)
  1801.     kmi = km.keymap_items.new("wm.context_toggle", 'B', 'PRESS')
  1802.     kmi.properties.data_path = "user_preferences.system.use_mipmaps"
  1803.     kmi = km.keymap_items.new("paint.toggle_add_multiply", 'D', 'PRESS')
  1804.     kmi = km.keymap_items.new("paint.display_tool_mode", 'F', 'PRESS',
  1805.         ctrl=True)
  1806.     kmi = km.keymap_items.new("paint.grid_texture", 'G', 'PRESS')
  1807.     kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
  1808.         shift=True)
  1809.     kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
  1810.         shift=True, ctrl=True)
  1811.     kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
  1812.         shift=True, alt=True)
  1813.     kmi = km.keymap_items.new("paint.straight_line", 'LEFTMOUSE', 'PRESS',
  1814.         shift=True, ctrl=True, alt=True)
  1815.     kmi = km.keymap_items.new("paint.change_selection", 'NUMPAD_MINUS', 'PRESS',
  1816.         ctrl=True)
  1817.     kmi.properties.mode = 'less'
  1818.     kmi = km.keymap_items.new("paint.change_selection", 'NUMPAD_PLUS', 'PRESS',
  1819.         ctrl=True)
  1820.     kmi.properties.mode = 'more'
  1821.     kmi = km.keymap_items.new("wm.context_set_enum", 'Q', 'PRESS')
  1822.     kmi.properties.data_path = "tool_settings.image_paint.brush.blend"
  1823.     kmi.properties.value = 'MIX'
  1824.     kmi = km.keymap_items.new("wm.context_toggle", 'R', 'PRESS')
  1825.     kmi.properties.data_path = "active_object.show_wire"
  1826.     kmi = km.keymap_items.new("paint.reload_image", 'R', 'PRESS',
  1827.         alt=True)
  1828. #    kmi = km.keymap_items.new("paint.sample_color_custom", 'RIGHTMOUSE', 'PRESS')
  1829.     kmi = km.keymap_items.new("wm.context_toggle", 'S', 'PRESS')
  1830.     kmi.properties.data_path = "active_object.data.use_paint_mask"
  1831.     kmi = km.keymap_items.new("paint.save_image", 'S', 'PRESS',
  1832.         alt=True)
  1833.     kmi = km.keymap_items.new("paint.brush_popup", 'W', 'PRESS')
  1834.     kmi = km.keymap_items.new("paint.toggle_image_paint", 'W', 'PRESS',
  1835.         shift=True)
  1836.    
  1837.     # add 3D View keymap entry
  1838.     km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
  1839.     kmi = km.keymap_items.new("object.add_default_image", 'Q', 'PRESS')
  1840.     kmi = km.keymap_items.new("object.default_material", 'X', 'PRESS',
  1841.         alt=True, ctrl=True)
  1842.    
  1843.     # add Mesh keymap entry
  1844.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
  1845. #    kmi = km.keymap_items.new("paint.cycle_blendtype", 'A', 'PRESS',
  1846. #        ctrl=True)
  1847.    
  1848.  #   print(bpy.context.window_manager.keyconfigs.default.keymaps.keys())
  1849.     # add UV Editor keymap entries, somehow UV Editor is not found, hrm...
  1850. #    km = bpy.context.window_manager.keyconfigs.default.keymaps['UV Editor']
  1851. #    kmi = km.keymap_items.new("uv.sync_selection", 'F', 'PRESS')
  1852. #    kmi = km.keymap_items.new("uv.toggle_uv_select_sync", 'F', 'PRESS',
  1853. #        shift=True)
  1854. #    kmi = km.keymap_items.new("wm.context_menu_enum", 'TAB', 'PRESS',
  1855. #        ctrl=True, shift=True)
  1856. #    kmi = km.keymap_items.new("paint.toggle_image_paint", 'W', 'PRESS',
  1857. #        shift=True)
  1858.    
  1859.     # deactivate to prevent clashing
  1860.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
  1861.     for kmi in km.keymap_items:
  1862.         if kmi.type == 'S' and not kmi.any and not kmi.shift and kmi.ctrl and kmi.alt and not kmi.oskey:
  1863.             kmi.active = False
  1864.    
  1865.     # add Window keymap entry
  1866.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
  1867.     kmi = km.keymap_items.new("wm.mass_link_append", 'F1', 'PRESS',
  1868.         alt=True)
  1869.     kmi = km.keymap_items.new("paint.reload_images", 'R', 'PRESS',
  1870.         alt=True, ctrl=True)
  1871.     kmi = km.keymap_items.new("wm.save_images", 'S','PRESS',
  1872.         alt=True, ctrl=True)
  1873.    
  1874.     # deactivate and remap to prevent clashing
  1875.     if bpy.context.user_preferences.inputs.select_mouse == 'RIGHT':
  1876.         right_mouse = ['RIGHTMOUSE', 'SELECTIONMOUSE']
  1877.     else: #'LEFT'
  1878.         right_mouse = ['RIGHTMOUSE', 'ACTIONMOUSE']
  1879.     km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
  1880.     for kmi in km.keymap_items:
  1881.         if kmi.type in right_mouse and kmi.alt and not kmi.ctrl and not kmi.shift:
  1882.             # deactivate
  1883.             kmi.active = False
  1884.     for kmi in km.keymap_items:
  1885.         if kmi.type in right_mouse and not kmi.alt and not kmi.ctrl and not kmi.shift:
  1886.             # remap
  1887.             kmi.alt = True
  1888.    
  1889.     # add menu entries
  1890.     bpy.types.VIEW3D_MT_edit_mesh.prepend(menu_func)
  1891.     bpy.types.VIEW3D_MT_edit_mesh_select_mode.append(menu_mesh_select_mode)
  1892.     bpy.types.VIEW3D_MT_snap.append(menu_snap)
  1893.  
  1894.  
  1895. def unregister():
  1896.     # remove menu entries
  1897.     bpy.types.VIEW3D_MT_edit_mesh.remove(menu_func)
  1898.     bpy.types.VIEW3D_MT_edit_mesh_select_mode.remove(menu_mesh_select_mode)
  1899.     bpy.types.VIEW3D_MT_snap.remove(menu_snap)
  1900.    
  1901.     # remove Image Paint keymap entries
  1902.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Image Paint']
  1903.     for kmi in km.keymap_items:
  1904.         if kmi.idname in ["paint.brush_popup", "paint.straight_line", "paint.toggle_alpha_mode", "paint.sample_color_custom", "paint.change_selection", "paint.cycle_blendtype", "paint.toggle_image_paint",
  1905.         "paint.toggle_add_multiply", "paint.grid_texture", "paint.display_tool_mode", "paint.reload_image", "paint.save_image"]:
  1906.             km.keymap_items.remove(kmi)
  1907.         elif kmi.idname == "wm.context_toggle":
  1908.             if getattr(kmi.properties, "data_path", False) in ["active_object.data.use_paint_mask", "active_object.show_wire", "user_preferences.system.use_mipmaps"]:
  1909.                 km.keymap_items.remove(kmi)
  1910.         elif kmi.idname == "wm.context_set_enum":
  1911.             if getattr(kmi.properties, "data_path", False) in ["tool_settings.image_paint.brush.blend"]:
  1912.                 km.keymap_items.remove(kmi)
  1913.    
  1914.     # remove 3D View keymap entry
  1915.     km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
  1916.     for kmi in km.keymap_items:
  1917.         if kmi.idname in ["object.add_default_image", "object.default_material"]:
  1918.             km.keymap_items.remove(kmi)
  1919.    
  1920.     # remove Mesh keymap entry
  1921.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Mesh']
  1922.     for kmi in km.keymap_items:
  1923.         if kmi.idname in ["paint.cycle_blendtype"]:
  1924.             km.keymap_items.remove(kmi)
  1925.  
  1926.     # remove UV Editor keymap entries
  1927.     #km = bpy.context.window_manager.keyconfigs.default.keymaps['UV Editor']
  1928.     #for kmi in km.keymap_items:
  1929.     #    if kmi.idname in ["wm.context_menu_enum", "uv.sync_selection", "uv.toggle_uv_select_sync", "paint.toggle_image_paint"]:
  1930.     #        km.keymap_items.remove(kmi)
  1931.  
  1932.     # remove Window keymap entry
  1933.     #km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
  1934.     #for kmi in km.keyamp_items:
  1935.     #    if kmi.idname in ["wm.mass_link_append", "paint.reload_images", "wm.save_images"]:
  1936.     #        km.keymap_items.remove(kmi)
  1937.    
  1938.     # remap and reactivate original items
  1939.     if bpy.context.user_preferences.inputs.select_mouse == 'RIGHT':
  1940.         right_mouse = ['RIGHTMOUSE', 'SELECTIONMOUSE']
  1941.     else: #'LEFT'
  1942.         right_mouse = ['RIGHTMOUSE', 'ACTIONMOUSE']
  1943.     km = bpy.context.window_manager.keyconfigs.default.keymaps['3D View']
  1944.     for kmi in km.keymap_items:
  1945.         if kmi.type in right_mouse and kmi.alt and not kmi.ctrl and not kmi.shift:
  1946.             if kmi.active:
  1947.                 # remap
  1948.                 kmi.alt = False
  1949.             else:
  1950.                 # reactivate
  1951.                 kmi.active = True
  1952.    
  1953.     # reactive original item
  1954.     km = bpy.context.window_manager.keyconfigs.default.keymaps['Window']
  1955.     for kmi in km.keymap_items:
  1956.         if kmi.type == 'S' and not kmi.any and not kmi.shift and kmi.ctrl and kmi.alt and not kmi.oskey:
  1957.             kmi.active = True
  1958.    
  1959.     # unregister classes
  1960.     remove_props()
  1961.     for c in classes:
  1962.         bpy.utils.unregister_class(c)
  1963.     try:
  1964.         del bpy.types.WindowManager.tpp
  1965.     except:
  1966.         pass
  1967.  
  1968.  
  1969. if __name__ == "__main__":
  1970.     register()
go to heaven