Skip to content

Commit

Permalink
Modification to Import/Export Family Configs (#1117)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Myles Smith and eirannejad authored Jan 14, 2021
1 parent d2d9bf5 commit a7a117d
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand All @@ -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 = '{} : {}'

Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -166,35 +189,46 @@ 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
fparam_group = sparam.fparam.Definition.ParameterGroup
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():
Expand All @@ -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)

Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -59,17 +68,20 @@
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"

ParamConfig = \
namedtuple(
'ParamConfig',
['name', 'bigroup', 'bitype', 'famcat',
'isinst', 'isreport', 'formula', 'default']
'isinst', 'isreport', 'formula', 'default','GUID']
)


Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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
)


Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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

0 comments on commit a7a117d

Please sign in to comment.