Material Creation/Override with Python

I’ve been trying to develop a code to create a material with specified color, based on the one shown here.

What I need is to create the material but, if it already exists, use the existing one (or override its color, ideally). I’ve created a Dynamo / Python routine for it, verifying the existence of the material with Dynamo and creating the material (or bypassing the existing ones) with Python. I must say that this is my first attempt at using Python or API related methods, so probably I’m missing something.

In Dynamo:

In Python:


# Código cópiado de Scott Crichton

import clr
clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import *
clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

clr.AddReference("RevitNodes")
import Revit
clr.ImportExtensions(Revit.Elements)

def ToRevitColor(dynamoColor):
	return Color(dynamoColor.Red, dynamoColor.Green, dynamoColor.Blue)

def ToDynamoObject(revitObject, isRevitOwned=False): # isRevitOwned should be False if Dynamo script created the object.
	return revitObject.ToDSType(isRevitOwned)

doc = DocumentManager.Instance.CurrentDBDocument

names = IN[0]
transparencies = IN[1]
colors = IN[2]
run_bool = IN[3]
mat_exist = UnwrapElement(IN[4])



newMaterials = []
for test_bool in run_bool:
	
	if test_bool == True:

		for mat_name, transparency, color in zip(names, transparencies, colors):
			TransactionManager.Instance.EnsureInTransaction(doc)
			new_mat_id = Material.Create(doc, mat_name)
			new_mat = doc.GetElement(new_mat_id)
			new_mat.Color = ToRevitColor(color)
			new_mat.Transparency = transparency
			newMaterials.append(ToDynamoObject(new_mat))
			TransactionManager.Instance.TransactionTaskDone()
	else:
		newMaterials.append(mat_exist)

		


OUT = newMaterials[0]

My problems are:

  • The first run returns an error in the Python node, saying Exception: The given value for name is already in use as a material element name.Parameter name: name
  • Despite the error message, the materials are created propperly, what makes me think somehow the Python scripts runs and reruns itself afterwards, finding the materials in the document, causing the error.
  • The second time I hit run, the script runs without errors. The Python script returns the bypassed materials, read by the Dynamo nodes.

What am I missing? I think that maybe finding the existing materials (or their absence) directly in Python would make much more sense, but I couldn’t figure it out.

did you ever get a solution to the problem you were having?

I am trying to write a similar script where the database originates from a text file, but i would like the data to update!

I get the following error
Exception: The given value for name is already in use as a material element name.
Parameter name: name

Hi, Shane, I’ve dropped this approach and went for a easier method. Instead of creating /checking the material every time the script ran, I copied the materials that existed in a template file I had set up previously (see the image bellow)


Note: I don’t remember the exact package the Document.BackgroundOpen node belongs to, but I believe it is easy to find out.
Also, if you post your code I can try to help you further. Perhaps a Try / Except could get rid of the error, but I’m not sure.

Best.

Thank you for your response @David_Doria. One of the drivers for trying to create this script would be the ability to update the material library in new projects or update the parameters of materials in existing projects. See my thread below

Shane, I believe this script will help you achieve what you want:

#Create or Update Materials based on List inputs

import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument
#Inputs

names = IN[0]
cols = IN[1]
costs = IN[2]

#New materials OUT list
newMaterials = []
#Overwritten materials OUT list
ovwMaterials = []

def ToRevitColor(dynamoColor):
	return Color(dynamoColor.Red, dynamoColor.Green, dynamoColor.Blue)
	
#Avoid instance input problem
if isinstance(names, list):
	names = names
else: names = [names]

if isinstance(cols, list):
	cols = cols
else: cols = [cols]

if isinstance(costs, list):
	costs = costs
else: costs = [costs]

#Iterate through the input names list
for n, cl, cs in zip(names,cols,costs):
	#Check if the material with the specified name already exists
	if Material.IsNameUnique(doc,n):
		#If name is unique, create material
		TransactionManager.Instance.EnsureInTransaction(doc)
		new_mat_id = Material.Create(doc, n)
		new_mat = doc.GetElement(new_mat_id)
		new_mat.Color = ToRevitColor(cl)
		new_mat_Cost = new_mat.get_Parameter(BuiltInParameter.ALL_MODEL_COST).Set(cs)

		TransactionManager.Instance.TransactionTaskDone()
		newMaterials.append(n)
	else:
		#If it already exists, collect it and modify it
		TransactionManager.Instance.EnsureInTransaction(doc)
		namePar = ParameterValueProvider(ElementId(BuiltInParameter.MATERIAL_NAME))
		fRule = FilterStringRule(namePar,FilterStringEquals(),n, True)
		filter = ElementParameterFilter(fRule)
		exist_mat = FilteredElementCollector(doc).OfClass(Material).WherePasses(filter).ToElements()
		#Iteration is necessary because the output of exist_mat is invariably a list
		for em in exist_mat:
			#Modify the Material Properties and Parameters
			em.Color = ToRevitColor(cl)
			emCost = em.get_Parameter(BuiltInParameter.ALL_MODEL_COST).Set(cs)
			ovwMaterials.append(n)
		TransactionManager.Instance.TransactionTaskDone()
		
OUT = newMaterials, ovwMaterials

It actually does what I initially intended to do, and a bit more. It checks if the material already exists in the document and, if not, creates a new one (with the properties and parameters as desired). If the material already exists, it overrides the properties and/or parameters of the element (Material). The problem I faced in my first attempt was that the methods to create and to modify a Material are different, so it is fundamental to find out if the element already exists or not at foremost. Since I don’t know which parameters and/or properties you intend to control, I left an example of each kind of operation. Also, the output of the node is set up to display which materials were created and which were overwritten. From this point it is easy to connect the materials with an outside source.

Best,
David

@David_Doria thank you! this is exactly what I was looking for!

I am hoping to expand this to include thermal and physical properties ( we do a lot of building physics analysis ), I’ll let you know how I get on !

1 Like

So…
I was able to expand the script to include all of the data on the Identity Data page except for the Material Class has anyone any idea where in the Revit API this value is located? it is the only parameter left on this page that I can’t find.

i have also started looking at adding the thermal data. But have hit a stumbling block, see screenshot below, it is returning “AttributeError: ‘NoneType’ object has no attribute ‘Set’

#Create or Update Materials based on List inputs
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument
#Inputs

names = IN[0]
thermalconductivity = IN[1]

#New materials OUT list
newMaterials = []
#Overwritten materials OUT list
ovwMaterials = []

	
#Avoid instance input problem
if isinstance(names, list):
	names = names
else: names = [names]

if isinstance(thermalconductivity, list):
	thermalconductivity = thermalconductivity
else: thermalconductivity = [thermalconductivity]

#Iterate through the input names list
for n, thmcd in zip(names,thermalconductivity):
	#Check if the material with the specified name already exists
	if Material.IsNameUnique(doc,n):
		#If name is unique, create material
		TransactionManager.Instance.EnsureInTransaction(doc)
		new_mat_id = Material.Create(doc, n)
		new_mat = doc.GetElement(new_mat_id)
		new_mat_ThermalConductivity = new_mat.get_Parameter(BuiltInParameter.PHY_MATERIAL_PARAM_THERMAL_CONDUCTIVITY).Set(thmcd)
		TransactionManager.Instance.TransactionTaskDone()
		newMaterials.append(n)
	else:
		#If it already exists, collect it and modify it
		TransactionManager.Instance.EnsureInTransaction(doc)
		namePar = ParameterValueProvider(ElementId(BuiltInParameter.MATERIAL_NAME))
		fRule = FilterStringRule(namePar,FilterStringEquals(),n, True)
		filter = ElementParameterFilter(fRule)
		exist_mat = FilteredElementCollector(doc).OfClass(Material).WherePasses(filter).ToElements()
		#Iteration is necessary because the output of exist_mat is invariably a list
		for em in exist_mat:
			#Modify the Material Properties and Parameters
			emThermalConductivity = em.get_Parameter(BuiltInParameter.PHY_MATERIAL_PARAM_THERMAL_CONDUCTIVITY).Set(thmcd)
			ovwMaterials.append(n)
		TransactionManager.Instance.TransactionTaskDone()
		
OUT = newMaterials, ovwMaterials

Shane, as it can be seen here the Material’s Class is a property of it, such as Color, not a Parameter. Try to set it with something like new_mat.MaterialClass(string) and check if it works. About the Thermal Conductivity parameter, I believe your problem lies in the nature of the Thermal Property. It does not belong to the API Material Class, but to the ThermalAsset Class, which is associated to the material object (similar to the Appearance asset).
Capturar

So, to achieve your goal, you must either create a new thermal asset to be associated with the material or associate an existing one and then modify its properties. You can refer to the ThermalAsset Class at the API Docs and this thread here that demonstrates the creation of a ThermalAsset with the API. After creating or selecting the Thermal Asset to be used, I think you’ll be able to associate it with the material via the ThermalAssetId Property of the material.

I am not completely sure this is all correct, given I haven’t done it, but I hope it helps you.
Best,

David

Shane,

This is awesome. Was wondering, how would you assign color to a surface pattern and for that matter a surface pattern to a material? Haven’t been able to assign either with success…

Looking forward to your input!

Marco J

Marco,

The code below will achieve your goal, I believe.

#PROTO_Material Creation / Management v2 (Color and Surface Pattern control)
# by David Dória
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument
#Inputs

names = IN[0]
cols = IN[1]
surfPatFor = IN[2]
surfPatForCl = IN[3]
surfPatBkg = IN[4]
surfPatBkgCl = IN[5]

#New materials OUT list
newMaterials = []
#Overwritten materials OUT list
ovwMaterials = []

def ToRevitColor(dynamoColor):
	return Color(dynamoColor.Red, dynamoColor.Green, dynamoColor.Blue)
	
#Avoid instance input problem
if isinstance(names, list):
	names = names
else: names = [names]

if isinstance(cols, list):
	cols = cols
else: cols = [cols]

if isinstance(surfPatFor, list):
	surfPatFor = surfPatFor
else: surfPatFor = [surfPatFor]

if isinstance(surfPatForCl, list):
	surfPatForCl = surfPatForCl
else: surfPatForCl = [surfPatForCl]

if isinstance(surfPatBkg, list):
	surfPatBkg = surfPatBkg
else: surfPatBkg = [surfPatBkg]

if isinstance(surfPatBkgCl, list):
	surfPatBkgCl = surfPatBkgCl
else: surfPatBkgCl = [surfPatBkgCl]

#Get Pattern Id
	

#Iterate through the input names list
for n, cl,spfor,spforC, spBkg, spBkgC  in zip(names,cols,surfPatFor,surfPatForCl,surfPatBkg,surfPatBkgCl):
	#Check if the material with the specified name already exists
	if Material.IsNameUnique(doc,n):
		#If name is unique, create material
		TransactionManager.Instance.EnsureInTransaction(doc)
		new_mat_id = Material.Create(doc, n)
		new_mat = doc.GetElement(new_mat_id)
		new_mat.Color = ToRevitColor(cl)
		new_mat.SurfaceForegroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spfor).Id
		
		new_mat.SurfaceForegroundPatternColor = ToRevitColor(spforC)
		
		new_mat.SurfaceBackgroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spBkg).Id
		
		new_mat.SurfaceBackgroundPatternColor = ToRevitColor(spBkgC)
		

		TransactionManager.Instance.TransactionTaskDone()
		newMaterials.append(n)
	else:
		#If it already exists, collect it and modify it
		TransactionManager.Instance.EnsureInTransaction(doc)
		namePar = ParameterValueProvider(ElementId(BuiltInParameter.MATERIAL_NAME))
		fRule = FilterStringRule(namePar,FilterStringEquals(),n, True)
		filter = ElementParameterFilter(fRule)
		exist_mat = FilteredElementCollector(doc).OfClass(Material).WherePasses(filter).ToElements()
		#Iteration is necessary because the output of exist_mat is invariably a list
		for em in exist_mat:
			#Modify the Material Properties and Parameters
			em.Color = ToRevitColor(cl)
			em.SurfaceForegroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spfor).Id
			
			em.SurfaceForegroundPatternColor = ToRevitColor(spforC)
			
			em.SurfaceBackgroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spBkg).Id
			
			em.SurfaceBackgroundPatternColor = ToRevitColor(spBkgC)
			
			ovwMaterials.append(n)
		TransactionManager.Instance.TransactionTaskDone()
		
OUT = newMaterials, ovwMaterials

Be aware that this is based on the Revit 2019 Materials, which have Foreground and Background surface patterns. If using an older version of Revit, replace the SurfaceForegroundPatternColor and the SurfaceForegroundPatternId with SurfacePatternColor and SurfacePatternId

Also, I suggest adding another input to control the Drafting or Model variable of the GetFillPatternElementByName(doc, FillPatternTarget.Drafting/Model,name) function, to facilitate the control via a external Database with different types of Fill Patterns.

Best,
David Dória

2 Likes

Hi @David_Doria

I have tried your code and it works with the code block input. But i’ve got an excel file with names of materials, patterns and colours. And I would like for these to be the input for the code. The information is currently in a List format with the colour being the output of the Color.ByARG node. Any tips on how to achieve this?

Inering, you should use the Data.Import.Excel node. I have updated the Python code to facilitate reading the RBGs from a string formatted as “0,0,0” in Excel. Use the images below to create your own routine.

#PROTO_Material Creation / Management v2 (Color and Surface Pattern control)
# by David Dória
import clr
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument
#Inputs

names = IN[0]
cols = IN[1]
surfPatFor = IN[2]
surfPatForCl = IN[3]
surfPatBkg = IN[4]
surfPatBkgCl = IN[5]

#New materials OUT list
newMaterials = []
#Overwritten materials OUT list
ovwMaterials = []

def ToRevitColor(dynamoColor):
	return Color(dynamoColor.Red, dynamoColor.Green, dynamoColor.Blue)

def ColorFromString(cString):
	cList = cString.split(",")
	return Color(int(cList[0]), int(cList[1]), int(cList[2]))
	
#Avoid instance input problem
if isinstance(names, list):
	names = names
else: names = [names]

if isinstance(cols, list):
	cols = cols
else: cols = [cols]

if isinstance(surfPatFor, list):
	surfPatFor = surfPatFor
else: surfPatFor = [surfPatFor]

if isinstance(surfPatForCl, list):
	surfPatForCl = surfPatForCl
else: surfPatForCl = [surfPatForCl]

if isinstance(surfPatBkg, list):
	surfPatBkg = surfPatBkg
else: surfPatBkg = [surfPatBkg]

if isinstance(surfPatBkgCl, list):
	surfPatBkgCl = surfPatBkgCl
else: surfPatBkgCl = [surfPatBkgCl]

#Get Pattern Id
	

#Iterate through the input names list
for n, cl,spfor,spforC, spBkg, spBkgC  in zip(names,cols,surfPatFor,surfPatForCl,surfPatBkg,surfPatBkgCl):
	#Check if the material with the specified name already exists
	if Material.IsNameUnique(doc,n):
		#If name is unique, create material
		TransactionManager.Instance.EnsureInTransaction(doc)
		new_mat_id = Material.Create(doc, n)
		new_mat = doc.GetElement(new_mat_id)
		new_mat.Color = ColorFromString(cl)
		new_mat.SurfaceForegroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spfor).Id
		
		new_mat.SurfaceForegroundPatternColor = ColorFromString(spforC)
		
		new_mat.SurfaceBackgroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spBkg).Id
		
		new_mat.SurfaceBackgroundPatternColor = ColorFromString(spBkgC)
		

		TransactionManager.Instance.TransactionTaskDone()
		newMaterials.append(n)
	else:
		#If it already exists, collect it and modify it
		TransactionManager.Instance.EnsureInTransaction(doc)
		namePar = ParameterValueProvider(ElementId(BuiltInParameter.MATERIAL_NAME))
		fRule = FilterStringRule(namePar,FilterStringEquals(),n, True)
		filter = ElementParameterFilter(fRule)
		exist_mat = FilteredElementCollector(doc).OfClass(Material).WherePasses(filter).ToElements()
		#Iteration is necessary because the output of exist_mat is invariably a list
		for em in exist_mat:
			#Modify the Material Properties and Parameters
			em.Color = ColorFromString(cl)
			em.SurfaceForegroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spfor).Id
			
			em.SurfaceForegroundPatternColor = ColorFromString(spforC)
			
			em.SurfaceBackgroundPatternId = FillPatternElement.GetFillPatternElementByName(doc, FillPatternTarget.Drafting,spBkg).Id
			
			em.SurfaceBackgroundPatternColor = ColorFromString(spBkgC)
			
			ovwMaterials.append(n)
		TransactionManager.Instance.TransactionTaskDone()
		
OUT = newMaterials, ovwMaterials

Excel Content format:


Dynamo:

1 Like

Hey, so i am very new at using Dynamo. I was trying to follow along but need some assistance. I am wanting to get all Existing Marerials and then add Thermal properties based on category or name. Like assign all Ceilings the same thermal property. Either from an excel database or just via the Autodesk Materials library. I think the above goes into how to create new materials with thermal properties but can i just change existing? or would you suggest it’s easiest to just make new?

Thanks!
-Virginia