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