From a7a117d36fd08a541e0183cd5cfa217e7e188af5 Mon Sep 17 00:00:00 2001 From: Myles Smith <m.smith@case.edu> Date: Thu, 14 Jan 2021 16:42:29 -0500 Subject: [PATCH] Modification to Import/Export Family Configs (#1117) * fixed host adder and added 2020.2.3 * added 2020.1.1 to hosts * Modified import and export buttons to handle shared parameters. The original behavior would assume all parameters were not shared. This commit detects shared parameters, and if they exist, creates another file and uses it to import them into Revit. * Resolves the issue of the Load Classification type being treated as a string. The yaml file assumes the Load Classification Type to be a string, so when using the original import button, the Load Classification in Revit would not populate. The export script attaches a string to the beginning of the Load Classification Type in the yaml file. When the import script comes across the notifier string, it imports the value as a Load Classification Type. A more general solution is necessary for other types of parameters that use a ElementId. Co-authored-by: Ehsan Iran-Nejad <eirannejad@gmail.com> --- .../Export Family Config.pushbutton/script.py | 74 ++++++++-- .../Import Family Config.pushbutton/script.py | 127 ++++++++++++------ 2 files changed, 153 insertions(+), 48 deletions(-) diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Export Family Config.pushbutton/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Export Family Config.pushbutton/script.py index 66adb03bd..3a0de4f77 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Export Family Config.pushbutton/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Export Family Config.pushbutton/script.py @@ -2,6 +2,9 @@ Family configuration file is a yaml file, providing info about the parameters and types defined in the family. +The shared parameters are exported to a txt file. +In the yaml file, the shared parameters are distinguished +by the presence of their GUID. The structure of this config file is as shown below: @@ -30,18 +33,25 @@ types: 24D"x36H": Shelf Height (Upper): 3'-0" + +Note: If a parameter is in the revit file and the yaml file, +but shared in one and family in the other, after import, +the parameter won't change. So if it was shared in the revit file, +but family in the yaml file, it will remain shared. """ #pylint: disable=import-error,invalid-name,broad-except # TODO: export parameter ordering +# add more to commit message from collections import OrderedDict -from pyrevit import revit, DB +from pyrevit import revit, DB, HOST_APP from pyrevit import forms from pyrevit import script +from Autodesk.Revit import Exceptions +from Autodesk.Revit.DB import ExternalDefinitionCreationOptions, Definition, Category from pyrevit.coreutils import yaml - logger = script.get_logger() output = script.get_output() @@ -54,9 +64,11 @@ PARAM_SECTION_REPORT = 'reporting' PARAM_SECTION_FORMULA = 'formula' PARAM_SECTION_DEFAULT = 'default' +PARAM_SECTION_GUID = 'GUID' # For shared parameters TYPES_SECTION_NAME = 'types' +LOAD_CLASS_NOTIFIER = '_ELECTRICAL_LOAD_CLASSIFICATION' FAMILY_SYMBOL_FORMAT = '{} : {}' @@ -85,6 +97,12 @@ def get_symbol_name(symbol_id): ) +def get_load_class_name(load_class_id): + # load_class_id is an element id + load_class = revit.doc.GetElement(load_class_id) + return "{0}({1})".format(LOAD_CLASS_NOTIFIER, load_class.Name) + + def get_param_typevalue(ftype, fparam): fparam_value = None # extract value by param type @@ -93,6 +111,11 @@ def get_param_typevalue(ftype, fparam): DB.ParameterType.FamilyType: fparam_value = get_symbol_name(ftype.AsElementId(fparam)) + elif fparam.StorageType == DB.StorageType.ElementId \ + and fparam.Definition.ParameterType == \ + DB.ParameterType.LoadClassification: + fparam_value = get_load_class_name(ftype.AsElementId(fparam)) + elif fparam.StorageType == DB.StorageType.String: fparam_value = ftype.AsString(fparam) @@ -166,7 +189,9 @@ def read_configs(selected_fparam_names, export_sparams = [SortableParam(x) for x in fm.GetParameters() if x.Definition.Name in selected_fparam_names] - # grab all parameter defs + shared_param = [] + + # Grab all parameter defs for sparam in sorted(export_sparams, reverse=True): fparam_name = sparam.fparam.Definition.Name fparam_type = sparam.fparam.Definition.ParameterType @@ -174,27 +199,36 @@ def read_configs(selected_fparam_names, fparam_isinst = sparam.fparam.IsInstance fparam_isreport = sparam.fparam.IsReporting fparam_formula = sparam.fparam.Formula + fparam_shared = sparam.fparam.IsShared + fparam_GUID = sparam.fparam.GUID if fparam_shared else None cfgs_dict[PARAM_SECTION_NAME][fparam_name] = { PARAM_SECTION_TYPE: str(fparam_type), PARAM_SECTION_GROUP: str(fparam_group), PARAM_SECTION_INST: fparam_isinst, PARAM_SECTION_REPORT: fparam_isreport, - PARAM_SECTION_FORMULA: fparam_formula + PARAM_SECTION_FORMULA: fparam_formula, + PARAM_SECTION_GUID: fparam_GUID } # get the family category if param is FamilyType selector if fparam_type == DB.ParameterType.FamilyType: cfgs_dict[PARAM_SECTION_NAME][fparam_name][PARAM_SECTION_CAT] = \ get_famtype_famcat(sparam.fparam) - + + # Check if the current family parameter is a shared parameter + if sparam.fparam.IsShared: + # Add to an array of sorted shared parameters + shared_param.append(sparam.fparam) + # include type configs? if include_types: include_type_configs(cfgs_dict, export_sparams) elif include_defaults: add_default_values(cfgs_dict, export_sparams) - - return cfgs_dict + + # The array of sorted shared parameters and dictionary of family parameters is returned + return shared_param, cfgs_dict def get_config_file(): @@ -217,6 +251,19 @@ def get_parameters(): ) or [] +def read_shared(sparams): + # Reads the shared parameters into a txt file + filename = HOST_APP.app.OpenSharedParameterFile() + definition_group = filename.Groups.Create("Exported Parameters") + for param in sparams: + external_definition_create_options = ExternalDefinitionCreationOptions(param.Definition.Name, param.Definition.ParameterType, GUID = param.GUID) + + try: + definition = definition_group.Definitions.Create(external_definition_create_options) + except Exceptions.ArgumentException: + forms.alert("A parameter with the same GUID already exists. Parameter: {} will be ignored.".format(param.Definition.Name)) + + if __name__ == '__main__': forms.check_familydoc(exitscript=True) @@ -240,10 +287,21 @@ def get_parameters(): yes=True, no=True ) - family_configs = \ + shared_params, family_configs = \ read_configs(family_params, include_types=inctypes, include_defaults=incdefault) logger.debug(family_configs) save_configs(family_configs, family_cfg_file) + + # Checks to make sure there are shared parameters to export + if len(shared_params) > 0: + # By default, the txt file will have the same name as the yaml file + defs_filename = family_cfg_file[:-4] + "txt" + saved = HOST_APP.app.SharedParametersFilename + filename = open(defs_filename,"w") + filename.close() + HOST_APP.app.SharedParametersFilename = defs_filename + read_shared(shared_params) + HOST_APP.app.SharedParametersFilename = saved \ No newline at end of file diff --git a/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Import Family Config.pushbutton/script.py b/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Import Family Config.pushbutton/script.py index 81ab27ba3..f2edbc25e 100644 --- a/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Import Family Config.pushbutton/script.py +++ b/extensions/pyRevitTools.extension/pyRevit.tab/Project.panel/ptools.stack/Family.pulldown/Import Family Config.pushbutton/script.py @@ -1,8 +1,16 @@ -"""Import family configurations from yaml file and modify current family. +"""Import family configurations (including shared parameters) +from yaml file and modify current family. Family configuration file is expected to be a yaml file, providing info about the parameters and types to be created. +The shared parameters are distinguished from the family parameters +by the presence of a GUID in the YAML file. +If a parameter in Revit has the same name as one in the YAML file, +the value or formula from the YAML file takes precedence. +The parameters in Revit that are not in the YAML file, +will not be affected by the import. + The structure of this config file is as shown below: parameters: @@ -32,17 +40,18 @@ Shelf Height (Upper): 3'-0" """ #pylint: disable=import-error,invalid-name,broad-except -# TODO: import parameter ordering -# TODO: merge configs on identical parameters + from collections import namedtuple from pyrevit import coreutils -from pyrevit import revit, DB +from pyrevit import revit, DB, HOST_APP from pyrevit import forms from pyrevit import script from pyrevit.coreutils import yaml +from Autodesk.Revit.DB.Electrical import ElectricalLoadClassification + logger = script.get_logger() output = script.get_output() @@ -59,9 +68,12 @@ PARAM_SECTION_REPORT = 'reporting' PARAM_SECTION_FORMULA = 'formula' PARAM_SECTION_DEFAULT = 'default' +PARAM_SECTION_GUID = 'GUID' TYPES_SECTION_NAME = 'types' +LOAD_CLASS_NOTIFIER = '_ELECTRICAL_LOAD_CLASSIFICATION' + FAMILY_SYMBOL_SEPARATOR = ' : ' TEMP_TYPENAME = "Default" @@ -69,7 +81,7 @@ namedtuple( 'ParamConfig', ['name', 'bigroup', 'bitype', 'famcat', - 'isinst', 'isreport', 'formula', 'default'] + 'isinst', 'isreport', 'formula', 'default','GUID'] ) @@ -112,9 +124,19 @@ def get_symbol_id(symbol_name): return fsym.Id +def get_load_class_id(load_class_name): + for lc in DB.FilteredElementCollector(revit.doc)\ + .OfClass(ElectricalLoadClassification)\ + .ToElements(): + if load_class_name == lc.Name: + return lc.Id + + # create a new load classification + return ElectricalLoadClassification.Create(revit.doc, load_class_name).Id + + def get_param_config(param_name, param_opts): # Extract parameter configurations from given dict - # extract configured values param_bip_cat = coreutils.get_enum_value( DB.BuiltInParameterGroup, param_opts.get(PARAM_SECTION_GROUP, DEFAULT_BIP_CATEGORY) @@ -134,6 +156,7 @@ def get_param_config(param_name, param_opts): param_opts.get(PARAM_SECTION_REPORT, 'false').lower() == 'true' param_formula = param_opts.get(PARAM_SECTION_FORMULA, None) param_default = param_opts.get(PARAM_SECTION_DEFAULT, None) + param_GUID = param_opts.get(PARAM_SECTION_GUID, "") if not param_bip_cat: logger.critical( @@ -155,7 +178,8 @@ def get_param_config(param_name, param_opts): isinst=param_isinst, isreport=param_isreport, formula=param_formula, - default=param_default + default=param_default, + GUID = param_GUID ) @@ -181,6 +205,14 @@ def set_fparam_value(pvcfg, fparam): fsym_id = get_symbol_id(pvcfg.value) fm.Set(fparam, fsym_id) + # can not use the types to find the value because yaml turns it into a string so need some sort of notifier + elif pvcfg.value.startswith(LOAD_CLASS_NOTIFIER): + first_letter = len(LOAD_CLASS_NOTIFIER) + 1 + load_class_name = pvcfg.value[first_letter:-1] + load_class_id = get_load_class_id(load_class_name) + + fm.Set(fparam, load_class_id) + elif fparam.StorageType == DB.StorageType.String: fm.Set(fparam, pvcfg.value) @@ -212,28 +244,35 @@ def ensure_param(param_name, param_opts): pcfg.isinst, pcfg.formula ) + isShared = pcfg.GUID != "" # When a parameter is shared (the GUID has a value and is not blank) fparam = revit.query.get_family_parameter(param_name, revit.doc) - if not fparam: - # create param in family doc - try: + try: + if fparam: + logger.info('The following parameter has been overridden by the YAML file: ' + fparam.Definition.Name) + if isShared: + defs = HOST_APP.app.OpenSharedParameterFile().Groups["Exported Parameters"].Definitions # Opens the txt file + matches = [d for d in defs if str(d.GUID) == pcfg.GUID] # Going through the yaml file looking for shared parameters + assert len(matches) == 1 # Will throw an error if nothing matches or too many matches + fparam = fm.AddParameter(matches[0],pcfg.bigroup,pcfg.isinst) + else: fparam = fm.AddParameter( pcfg.name, pcfg.bigroup, pcfg.famcat if pcfg.famcat else pcfg.bitype, pcfg.isinst ) - except Exception as addparam_ex: - if pcfg.famcat: - failed_params.append(pcfg.name) - logger.error( - 'Error creating parameter: %s\n' - 'This parameter is a nested family selector. ' - 'Make sure at least one nested family of type "%s" ' - 'is already loaded in this family. | %s', - pcfg.name, - pcfg.famcat.Name, - addparam_ex - ) + except Exception as addparam_ex: + if pcfg.famcat: + failed_params.append(pcfg.name) + logger.error( + 'Error creating parameter: %s\n' + 'This parameter is a nested family selector. ' + 'Make sure at least one nested family of type "%s" ' + 'is already loaded in this family. | %s', + pcfg.name, + pcfg.famcat.Name, + addparam_ex + ) logger.debug('Created: %s', fparam) @@ -279,7 +318,7 @@ def ensure_params(fconfig): # ensure all defined parameters exist param_cfgs = fconfig.get(PARAM_SECTION_NAME, None) if param_cfgs: - for pname, popts in param_cfgs.items(): + for pname, popts in param_cfgs.items(): # going through the parameters in the yaml file ensure_param(pname, popts) @@ -354,23 +393,31 @@ def load_configs(parma_file): if __name__ == '__main__': forms.check_familydoc(exitscript=True) - family_cfg_file = get_config_file() if family_cfg_file: family_mgr = revit.doc.FamilyManager - family_configs = load_configs(family_cfg_file) - logger.debug(family_configs) - with revit.Transaction('Import Params from Config'): - # remember current type - # if family does not have type, create a temp type - # otherwise setting formula will fail - ctype = family_mgr.CurrentType - if not ctype: - ctype = family_mgr.NewType(TEMP_TYPENAME) - - ensure_params(family_configs) - ensure_types(family_configs) - - # restore current type - if ctype.Name != TEMP_TYPENAME: - family_mgr.CurrentType = ctype + family_configs = load_configs(family_cfg_file) # Dictionary with family parameters in yaml file + + defs_filename = family_cfg_file[:-4] + "txt" + saved = HOST_APP.app.SharedParametersFilename + HOST_APP.app.SharedParametersFilename = defs_filename + try: + logger.debug(family_configs) + with revit.Transaction('Import Params from Config'): + # Remember current type + # If family does not have type, create a temp type, + # otherwise setting formula will fail + ctype = family_mgr.CurrentType + if not ctype: + ctype = family_mgr.NewType(TEMP_TYPENAME) + + ensure_params(family_configs) + ensure_types(family_configs) + + # Restore current type + if ctype.Name != TEMP_TYPENAME: + family_mgr.CurrentType = ctype + finally: + # Even if there is an error somewhere else in the file, + # the rest of the file will always be imported + HOST_APP.app.SharedParametersFilename = saved \ No newline at end of file