Last active
November 10, 2025 03:14
-
-
Save yiskang/d394dcb3d95b68389e274cffdabe56ba to your computer and use it in GitHub Desktop.
Revit addin for doing similar like Revit UI function "Import Family Types"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ///////////////////////////////////////////////////////////////////// | |
| // 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; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ////////////////////////////////////////////////////////////////////// | |
| // 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