Skip to content

Instantly share code, notes, and snippets.

@yiskang
Last active November 10, 2025 03:14
Show Gist options
  • Select an option

  • Save yiskang/d394dcb3d95b68389e274cffdabe56ba to your computer and use it in GitHub Desktop.

Select an option

Save yiskang/d394dcb3d95b68389e274cffdabe56ba to your computer and use it in GitHub Desktop.
Revit addin for doing similar like Revit UI function "Import Family Types"
/////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Developer Advocacy and Support
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
/////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;
namespace RevitAddinSandboxNet48
{
[Transaction(TransactionMode.Manual)]
public class Command : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
var document = commandData.Application.ActiveUIDocument.Document;
if (!document.IsFamilyDocument)
{
message = "Open a family document.";
return Result.Failed;
}
var familyTypesToDelete = new List<FamilyType>();
foreach (FamilyType familyType in document.FamilyManager.Types)
{
familyTypesToDelete.Add(familyType);
}
var parser = new FamilyTypeTableParser();
parser.Load(@"C:\Users\UserA\Desktop\MyFamilyTypes.txt");
using(var txGroup = new TransactionGroup(document, "Import Family Types (API)"))
{
txGroup.Start();
//Method 1: Create family types & apply data in one go
using (var tx = new Transaction(document, "Create family types & Apply type parameter data"))
{
tx.Start();
parser.ImportFamilyTypes(document.FamilyManager);
tx.Commit();
}
using (var tx2 = new Transaction(document, "Delete old existing types"))
{
tx2.Start();
foreach (FamilyType familyTypeToDelete in familyTypesToDelete)
{
document.FamilyManager.CurrentType = familyTypeToDelete;
document.FamilyManager.DeleteCurrentType();
}
document.FamilyManager.CurrentType = document.FamilyManager.Types.Cast<FamilyType>().FirstOrDefault();
tx2.Commit();
}
////Method 2: Create family types first, then apply data
//using (var tx2 = new Transaction(document, "Apply type parameter data"))
//{
// tx2.Start();
// parser.ApplyAllRowsToFamilyTypes(document.FamilyManager);
// tx2.Commit();
//}
//using (var tx3 = new Transaction(document, "Delete old existing types"))
//{
// tx3.Start();
// foreach (FamilyType familyTypeToDelete in familyTypesToDelete)
// {
// document.FamilyManager.CurrentType = familyTypeToDelete;
// document.FamilyManager.DeleteCurrentType();
// }
// document.FamilyManager.CurrentType = document.FamilyManager.Types.Cast<FamilyType>().FirstOrDefault();
// tx3.Commit();
//}
//using (var tx3 = new Transaction(document, "Delete old existing types"))
//{
// tx3.Start();
// foreach (FamilyType familyTypeToDelete in familyTypesToDelete)
// {
// document.FamilyManager.CurrentType = familyTypeToDelete;
// document.FamilyManager.DeleteCurrentType();
// }
// document.FamilyManager.CurrentType = document.FamilyManager.Types.Cast<FamilyType>().FirstOrDefault();
// tx3.Commit();
//}
txGroup.Assimilate();
}
TaskDialog.Show("ADN", "Faimly Types Imported");
return Result.Succeeded;
}
}
}
//////////////////////////////////////////////////////////////////////
// Copyright (c) Autodesk, Inc. All rights reserved
// Written by Developer Advocacy and Support
// Enhanced by GitHub Copilot GTP-5 on 2025-11-07
//
// Permission to use, copy, modify, and distribute this software in
// object code form for any purpose and without fee is hereby granted,
// provided that the above copyright notice appears in all copies and
// that both that copyright notice and the limited warranty and
// restricted rights notice below appear in all supporting
// documentation.
//
// AUTODESK PROVIDES THIS PROGRAM "AS IS" AND WITH ALL FAULTS.
// AUTODESK SPECIFICALLY DISCLAIMS ANY IMPLIED WARRANTY OF
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR USE. AUTODESK, INC.
// DOES NOT WARRANT THAT THE OPERATION OF THE PROGRAM WILL BE
// UNINTERRUPTED OR ERROR FREE.
//////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Autodesk.Revit.DB;
namespace RevitAddinSandboxNet48
{
/// <summary>
/// Parses a CSV-like definition table describing Revit family types and their parameter values.
/// Header format per column: Name##Quantity##UnitToken.
/// Quantity controls interpretation (conversion, numeric vs text) and UnitToken drives unit conversion.
/// First (or empty) column is treated as the TypeKey (family type name).
/// Lines starting with '#' or '//' are ignored as comments.
/// Note the format follows Revit Lookup Table at https://help.autodesk.com/view/RVT/2024/ENU/?guid=GUID-DD4D26EB-0827-4EDB-8B1F-E591B9EA8CA0
/// </summary>
public class FamilyTypeTableParser
{
/// <summary>
/// Logical quantity categories decoded from header meta data.
/// </summary>
public enum QuantityKind { Number, Length, Area, Volume, Angle, Other }
/// <summary>
/// Metadata for a single column header in the source table.
/// </summary>
public class ColumnMeta
{
/// <summary>Original unparsed header token.</summary>
public string RawHeader;
/// <summary>Clean parameter / column name (Definition name in Revit).</summary>
public string Name;
/// <summary>Interpreted quantity kind.</summary>
public QuantityKind Quantity;
/// <summary>Unit token (e.g. FEET, METERS, DEGREES, GENERAL, BOOLEAN, etc.).</summary>
public string UnitToken;
}
/// <summary>
/// Represents a single row of family type data mapped by column name.
/// </summary>
public class FamilyTypeRow
{
/// <summary>The key (type name) identified by the first column.</summary>
public string TypeKey;
/// <summary>Map of parameter name to raw string value.</summary>
public Dictionary<string, string> Values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
private readonly List<ColumnMeta> _columns = new List<ColumnMeta>();
private readonly List<FamilyTypeRow> _rows = new List<FamilyTypeRow>();
/// <summary>All parsed column metadata.</summary>
public IReadOnlyList<ColumnMeta> Columns { get { return _columns; } }
/// <summary>All parsed data rows.</summary>
public IReadOnlyList<FamilyTypeRow> Rows { get { return _rows; } }
/// <summary>
/// Load and parse the definition file.
/// Expected CSV with a header followed by one or more data rows.
/// Empty header cell or first header becomes the special TypeKey column.
/// </summary>
/// <param name="filePath">Full path to the table text file.</param>
public void Load(string filePath)
{
_columns.Clear();
_rows.Clear();
if (!File.Exists(filePath))
throw new FileNotFoundException("File not found.", filePath);
// Filter out blank lines and comment lines.
var lines = File.ReadAllLines(filePath)
.Where(l => !string.IsNullOrWhiteSpace(l))
.Where(l => !l.TrimStart().StartsWith("#") && !l.TrimStart().StartsWith("//"))
.ToList();
if (lines.Count < 2)
throw new InvalidOperationException("File must have header and at least one data row.");
// Parse header cells into column meta definitions.
var headerParts = SplitLine(lines[0]);
for (int i = 0; i < headerParts.Count; i++)
{
var h = headerParts[i].Trim();
ColumnMeta meta;
if (string.IsNullOrEmpty(h))
{
// Treat empty header as the TypeKey column.
meta = new ColumnMeta { RawHeader = "", Name = "TypeKey", Quantity = QuantityKind.Other, UnitToken = "GENERAL" };
}
else
{
meta = ParseHeader(h);
}
_columns.Add(meta);
}
// Parse each data line into row objects.
for (int r = 1; r < lines.Count; r++)
{
var parts = SplitLine(lines[r]);
if (parts.Count == 0) continue;
var row = new FamilyTypeRow();
for (int c = 0; c < _columns.Count; c++)
{
string val = c < parts.Count ? parts[c].Trim() : ""; // Missing cells treated as empty.
var col = _columns[c];
if (col.Name == "TypeKey") row.TypeKey = val;
else row.Values[col.Name] = val;
}
_rows.Add(row);
}
}
/// <summary>
/// Parse a single header token in the format Name##Quantity##Unit.
/// Quantity and Unit default to OTHER / GENERAL if omitted.
/// </summary>
/// <param name="token">Raw header token.</param>
/// <returns>Column metadata parsed from the token.</returns>
private ColumnMeta ParseHeader(string token)
{
var parts = token.Split(new[] { "##" }, StringSplitOptions.None);
string name = parts[0].Trim();
string type = parts.Length > 1 ? parts[1].Trim().ToUpperInvariant() : "OTHER";
string unit = parts.Length > 2 ? parts[2].Trim().ToUpperInvariant() : "GENERAL";
QuantityKind kind = QuantityKind.Other;
if (type == "NUMBER") kind = QuantityKind.Number;
else if (type == "LENGTH") kind = QuantityKind.Length;
else if (type == "AREA") kind = QuantityKind.Area;
else if (type == "VOLUME") kind = QuantityKind.Volume;
else if (type == "ANGLE") kind = QuantityKind.Angle;
return new ColumnMeta
{
RawHeader = token,
Name = name,
Quantity = kind,
UnitToken = unit
};
}
/// <summary>
/// Basic comma split. (No support for quoted values.)
/// </summary>
/// <param name="line">Raw line to split.</param>
/// <returns>List of cell values.</returns>
private List<string> SplitLine(string line) { return line.Split(',').ToList(); }
/// <summary>
/// Find a previously parsed row by its TypeKey (case-insensitive).
/// </summary>
/// <param name="key">Family type key (name) to search for.</param>
/// <returns>The matching row or null if not found.</returns>
public FamilyTypeRow FindByTypeKey(string key)
{
return _rows.FirstOrDefault(r => string.Equals(r.TypeKey, key, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Enumerate all TypeKeys from parsed rows.
/// </summary>
/// <returns>Enumeration of all parsed type keys.</returns>
public IEnumerable<string> GetAllTypeKeys()
{
foreach (var r in _rows) yield return r.TypeKey;
}
/// <summary>
/// Ensure that all parsed TypeKeys exist as FamilyTypes in the FamilyManager.
/// Creates new types if missing.
/// </summary>
/// <param name="fm">Active <see cref="FamilyManager"/>.</param>
public void EnsureFamilyTypes(FamilyManager fm)
{
var existing = new HashSet<string>(fm.Types.Cast<FamilyType>().Select(t => t.Name), StringComparer.OrdinalIgnoreCase);
foreach (var key in GetAllTypeKeys())
{
if (!existing.Contains(key))
fm.NewType(key);
}
}
/// <summary>
/// Apply a single parsed row's parameter values to the provided target FamilyType.
/// Performs type/quantity/unit based conversions before setting parameters.
/// </summary>
/// <param name="fm">Family manager controlling the family document.</param>
/// <param name="row">Parsed data row to apply.</param>
/// <param name="targetType">Target existing family type.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="row"/> or <paramref name="targetType"/> is null.</exception>
public void ApplyRowToFamilyType(FamilyManager fm, FamilyTypeRow row, FamilyType targetType)
{
if (row == null) throw new ArgumentNullException(nameof(row));
if (targetType == null) throw new ArgumentNullException(nameof(targetType));
fm.CurrentType = targetType; // Set active type.
foreach (var col in _columns)
{
if (col.Name == "TypeKey") continue; // Skip the key column.
string raw;
if (!row.Values.TryGetValue(col.Name, out raw)) continue; // Skip missing values.
var fp = fm.get_Parameter(col.Name);
if (fp == null || fp.IsReadOnly) continue; // Only set existing, writable parameters.
SetFamilyParameterValue(fm, fp, raw, col);
}
}
/// <summary>
/// Apply all parsed rows to corresponding existing family types.
/// Relies on name matching; rows with no matching type are skipped.
/// </summary>
/// <param name="fm">Family manager controlling the family document.</param>
public void ApplyAllRowsToFamilyTypes(FamilyManager fm)
{
var map = fm.Types.Cast<FamilyType>().ToDictionary(t => t.Name, t => t, StringComparer.OrdinalIgnoreCase);
foreach (var row in _rows)
{
FamilyType ft;
if (!map.TryGetValue(row.TypeKey, out ft)) continue;
ApplyRowToFamilyType(fm, row, ft);
}
}
/// <summary>
/// Create family types and apply parameter values by using the data row parsed from the definition file.
/// Creates new types if missing.
/// </summary>
/// <param name="fm">Active <see cref="FamilyManager"/>.</param>
public void ImportFamilyTypes(FamilyManager fm)
{
var existing = new HashSet<string>(fm.Types.Cast<FamilyType>().Select(t => t.Name), StringComparer.OrdinalIgnoreCase);
foreach (var row in _rows)
{
var key = row.TypeKey;
if (!existing.Contains(key))
{
FamilyType ft = fm.NewType(key);
ApplyRowToFamilyType(fm, row, ft);
}
}
}
/// <summary>
/// Core routine to set a Revit family parameter from a raw string value using column metadata.
/// Handles text, Yes/No, numeric (double & int) and performs unit conversions.
/// </summary>
/// <param name="fm">Family manager instance.</param>
/// <param name="fp">Parameter to set.</param>
/// <param name="raw">Raw string value.</param>
/// <param name="meta">Column metadata (quantity + unit).</param>
private void SetFamilyParameterValue(FamilyManager fm, FamilyParameter fp, string raw, ColumnMeta meta)
{
ForgeTypeId dataType = fp.Definition.GetDataType();
// Text-like parameter: set string directly.
if (IsText(dataType, meta))
{
fm.Set(fp, raw);
return;
}
// Boolean parameter: treat multiple representations as true.
if (IsYesNo(dataType, meta))
{
fm.Set(fp, ParseBool(raw) ? 1 : 0);
return;
}
double d;
if (TryParseDouble(raw, out d))
{
// Length without explicit quantity but target parameter is a length.
if (dataType == SpecTypeId.Length && meta.Quantity == QuantityKind.Other)
{
d = UnitUtils.ConvertToInternalUnits(d, fp.GetUnitTypeId());
}
else
{
// Convert based on declared quantity & unit token.
d = ConvertByHeader(meta, d);
}
fm.Set(fp, d);
return;
}
int iv;
if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out iv))
{
fm.Set(fp, iv);
}
}
/// <summary>
/// Determines if a parameter should be treated as text based on Revit data type or header fallback.
/// </summary>
/// <param name="dt">Revit data type id.</param>
/// <param name="m">Column metadata.</param>
/// <returns>True if parameter is considered text; otherwise false.</returns>
private bool IsText(ForgeTypeId dt, ColumnMeta m)
{
// Text spec id + fallback for unspecified (GENERAL/OTHER)
return dt == SpecTypeId.String.Text || (m.Quantity == QuantityKind.Other && m.UnitToken == "GENERAL");
}
/// <summary>
/// Determines if a parameter should be treated as boolean (Yes/No).
/// </summary>
/// <param name="dt">Revit data type id.</param>
/// <param name="m">Column metadata.</param>
/// <returns>True if parameter is considered boolean; otherwise false.</returns>
private bool IsYesNo(ForgeTypeId dt, ColumnMeta m)
{
return dt == SpecTypeId.Boolean.YesNo || (m.Quantity == QuantityKind.Other && m.UnitToken == "BOOLEAN");
}
/// <summary>
/// Attempt to parse a double (culture invariant) supporting thousands separators & floats.
/// </summary>
/// <param name="raw">Raw numeric string.</param>
/// <param name="v">Parsed double value on success.</param>
/// <returns>True if parse succeeded; otherwise false.</returns>
private bool TryParseDouble(string raw, out double v)
{
return double.TryParse(raw, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out v);
}
/// <summary>
/// Parse a flexible boolean representation (1, true, yes treated as true).
/// </summary>
/// <param name="raw">Raw token.</param>
/// <returns>True if token represents a true value; otherwise false.</returns>
private bool ParseBool(string raw)
{
raw = raw.Trim();
return raw == "1" || raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw.Equals("yes", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Convert numeric value according to header quantity & unit token into internal Revit units.
/// </summary>
/// <param name="meta">Column metadata guiding conversion.</param>
/// <param name="value">Original numeric value.</param>
/// <returns>Value converted to internal units (or unchanged if no conversion applies).</returns>
private double ConvertByHeader(ColumnMeta meta, double value)
{
if (meta.Quantity == QuantityKind.Length)
return UnitUtils.ConvertToInternalUnits(value, MapLengthUnit(meta.UnitToken));
if (meta.Quantity == QuantityKind.Area)
return UnitUtils.ConvertToInternalUnits(value, MapAreaUnit(meta.UnitToken));
if (meta.Quantity == QuantityKind.Volume)
return UnitUtils.ConvertToInternalUnits(value, MapVolumeUnit(meta.UnitToken));
if (meta.Quantity == QuantityKind.Angle)
return UnitUtils.ConvertToInternalUnits(value, MapAngleUnit(meta.UnitToken));
if (meta.Quantity == QuantityKind.Number && meta.UnitToken == "PERCENTAGE")
return value > 1.0 ? value / 100.0 : value; // Accept either 0-1 or 0-100 inputs.
return value; // No conversion.
}
/// <summary>Map header length unit token to a ForgeTypeId.</summary>
/// <param name="unit">Length unit token (e.g. FEET, METERS).</param>
/// <returns>Length unit ForgeTypeId.</returns>
private ForgeTypeId MapLengthUnit(string unit)
{
if (unit == "MILLIMETERS") return UnitTypeId.Millimeters;
if (unit == "CENTIMETERS") return UnitTypeId.Centimeters;
if (unit == "METERS") return UnitTypeId.Meters;
if (unit == "INCHES") return UnitTypeId.Inches;
if (unit == "FEET") return UnitTypeId.Feet;
return UnitTypeId.Millimeters; // Sensible default.
}
/// <summary>Map header area unit token to a ForgeTypeId.</summary>
/// <param name="unit">Area unit token.</param>
/// <returns>Area unit ForgeTypeId.</returns>
private ForgeTypeId MapAreaUnit(string unit)
{
if (unit == "SQUARE_MILLIMETERS") return UnitTypeId.SquareMillimeters;
if (unit == "SQUARE_METERS") return UnitTypeId.SquareMeters;
if (unit == "SQUARE_FEET") return UnitTypeId.SquareFeet;
if (unit == "SQUARE_INCHES") return UnitTypeId.SquareInches;
return UnitTypeId.SquareMeters;
}
/// <summary>Map header volume unit token to a ForgeTypeId.</summary>
/// <param name="unit">Volume unit token.</param>
/// <returns>Volume unit ForgeTypeId.</returns>
private ForgeTypeId MapVolumeUnit(string unit)
{
if (unit == "CUBIC_MILLIMETERS") return UnitTypeId.CubicMillimeters;
if (unit == "CUBIC_METERS") return UnitTypeId.CubicMeters;
if (unit == "CUBIC_FEET") return UnitTypeId.CubicFeet;
if (unit == "LITERS") return UnitTypeId.Liters;
return UnitTypeId.CubicMeters;
}
/// <summary>Map header angle unit token to a ForgeTypeId.</summary>
/// <param name="unit">Angle unit token.</param>
/// <returns>Angle unit ForgeTypeId.</returns>
private ForgeTypeId MapAngleUnit(string unit)
{
if (unit == "DEGREES") return UnitTypeId.Degrees;
if (unit == "RADIANS") return UnitTypeId.Radians;
return UnitTypeId.Degrees;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment