Friday, March 14, 2025

make material symmetric and context manager example in python (Blender 2.79)

this is an example using a context manager in Bender to help with switching modes for running a tool.

its also an example of setting a material on a face using python in Blender.  the mesh is assumed to be topologically symmetric and it only works with the source side in +x.

the tool may have bugs so please use/modify at your own risk.  (tested in Blender 2.79)

before:

after:

import bpy
from mathutils import Vector
from math import sqrt
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG) #without this info logs wouldnt show in


class EnterModeContext(object):
	"""gives ability to enter a needed mode > then run needed > then return to original mode

	ex entering into edit mode:

	#subdivide selected mesh in edit mode
	with EnterModeContext("EDIT"):
		bpy.ops.mesh.subdivide()
	"""
	def __init__(self, toMode):
		"""
		@param toMode (str) mode to go into ex: 'EDIT' or 'POSE' etc
		"""
		self._toMode = toMode #mode to go into

	def __enter__(self):
		#print("enter")
		self._currentMode = bpy.context.object.mode

		#go into to mode if not already in it 
		if self._currentMode != self._toMode:
			bpy.ops.object.mode_set(mode=self._toMode)

	def __exit__(self, ext, exv, trb):
		#print("exiting")
		logger.info("ext:{}".format(ext))
		logger.info("exv:{}".format(exv))
		logger.info("trb:{}".format(trb))
		#return into starting mode if not already in it 
		if self._currentMode != self._toMode:
			bpy.ops.object.mode_set(mode=self._currentMode)		
		return True





def makeMaterialSymmetric(meshName):
	"""make materials symmetric. given a topology symmetric mesh.
	Args:
		meshName (str) data object name for mesh we want to make material symmetric

	Returns:
		(bool) whether successfully made material symmetric
	"""
	epsilon = 0.000001 #how close is enough to say found a matched point
	numFaces = len(bpy.data.objects[meshName].data.polygons)
	faceMirrorData = []

	for faceIndex in range(0, numFaces):
		facePos = bpy.data.objects[meshName].data.polygons[faceIndex].center	

		#we are only editing right side faces
		if facePos >= 0:
			continue

		#loop through all faces trying to find closest face to mirror point
		#negate x to get left side mirror face position
		mirrorPos = Vector((-1*facePos.x, facePos.y, facePos.z))
		for f in range(0, numFaces):
			fPos = bpy.data.objects[meshName].data.polygons[f].center
			#ignore left side faces as they are not on right side of mesh
			if fPos <= 0:
				continue
			#check if found a mirrored face
			dist = sqrt((fPos.x - mirrorPos.x)**2 + (fPos.y - mirrorPos.y)**2 + (fPos.z - mirrorPos.z)**2)
			#if distance too big we know we havent found our mirrored face yet
			if dist > epsilon:
				continue

			#we found our mirrored face
			dat = dict(faceIndex=faceIndex, mirrorIndex=f)
			faceMirrorData.append(dat)
			break #exit loop as we dont need to check any others

		#continue to next left side face we want to search for a mirror
	#done with loops

	#appy the material on mirror side
	for data in faceMirrorData:
		sourceIndex = data.get('faceIndex', None)
		destinationIndex = data.get('mirrorIndex', None)
		if (not sourceIndex) or (not destinationIndex):
			print("cannot find a mirrored face for source face {}. mesh is more than likely not symmetric".format(sourceIndex))
			continue
		sourceMaterialIndex = bpy.data.objects[meshName].data.polygons[sourceIndex].material_index
		#set mirror side material
		bpy.data.objects[meshName].data.polygons[destinationIndex].material_index = sourceMaterialIndex

	return True


"""
import imp
testTool = imp.load_source("tool","/Users/Nathaniel/Documents/src_blender/python/snippets/texture_snippets.py") #for mac. change to path to python file

with testTool.EnterModeContext("OBJECT"):
	testTool.makeMaterialSymmetric("Sphere")
""" 
Thanks for looking