Files
speckle-sharp-connectors/Connectors/Revit/Speckle.Connectors.RevitShared/HostApp/RevitFamilyBaker.cs
T
Jedd Morgan 7860c44f4e feat(api)!: Implement new packfile based sends via SendPipline (aka DuckDB changes) (#1277)
* Dim/quack lets go (#1275)

* Add model ingestion to sharp connectors

* correct ingestion message

* Progress

* grasshopper

* GH exception messages

* fix GH

* file names

* revit file name

* grasshopper file names

* etabs file names

* delete tests

* tekla maybe

* ingestion  scope

* bad boolean logic

* Longer TimeSpan

* wip upload pipe

* 10s

* passthrough ingestion id

* happy hack time: prevent ingestion completion

this is handled server-side in the processing logic.

* add packfile send endpoint detection and routing

Route to SendViaPackfile when the server supports the upload-signing
endpoint (POST probe, 404 = unsupported) and a continuous traversal
builder is registered.

* Adds Continuous Traversal Builder

Introduces a Continuous Traversal Builder to manage the conversion and processing of Revit elements within a Send Pipeline.

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* feat(api): DI Refactor for Duck DB + Gergo's API endpoint changes (#1282)

* Di

* undo accidental change

* Feat (duck): dui ingestion updates post upload (#1295)

* Pass optional ingestion id to DUI

* Make ingestion id null for the SendViaIngestion, see the note :)

* feat!: Duckdev progress reporitng (#1296)

* Di

* throwaway from laptop

* Progress reporting

* Use matching logger

* Revit and revert rhino unpacker progress

* more revertion

* make pr even cleaner

* and this one

* fix build issues with other connectors

* SDK nuget (#1299)

* Bump to 3.14.0-alpha.2

* Feat(duck): grasshopper (#1297)

* Duck x Grasshopper - who would win?

* Fix registration for new builder

* missing imports

* return version id grasshopper

* Align sync resource to sync

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Bump SDK

* feat(importer): rhino file importer changes for packfile (#1301)

* rhino importer changes

* correct deps

* Bump SDK

* Fix build issues

* ditto

* Fix build issue

* Lower standards

* Fix build

* feat: duck for acad, civil, navis, tekla, etabs (#1300)

* duck: acad, civil, etabs, tekla, navis and bump channels to 10.0.0

* notes

* fix conflicts

* more conflicts

* Ready for testing

* fix(duck): Fix send caching (#1302)

* potential fix

* undo-rhino chnages

* fix xml comment

* amended comment

* revit

* Fix build

* Aligned converting message

* fix: reoccurring object references

* Bump sdk and resolve merge conflict issues

* Merge pull request #1317 from specklesystems/jrm/importer-tracing

feat(otel): Tracing and OTEL changes for Rhino importer

* Fix revit linked model progress (#1312)

* Revert otel packages

* bump SDK

* Trace unpacking groups

* Align trace context nullability with app

* Disable send caching in Navisworks

* comments

* Update FileimportPayload.cs

* fix using directive

---------

Co-authored-by: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com>

* Fix merge conflicts

---------

Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sebastian Witt <sebastian.witt@rwth-aachen.de>
2026-04-08 10:07:56 +01:00

622 lines
20 KiB
C#

using System.IO;
using Autodesk.Revit.Creation;
using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Structure;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.Common.Conversion;
using Speckle.Converters.Common;
using Speckle.Converters.Common.Objects;
using Speckle.Converters.RevitShared.Helpers;
using Speckle.Converters.RevitShared.Settings;
using Speckle.DoubleNumerics;
using Speckle.Objects.Other;
using Speckle.Sdk;
using Speckle.Sdk.Common;
using Speckle.Sdk.Common.Exceptions;
using Speckle.Sdk.Models;
using Speckle.Sdk.Models.Collections;
using Speckle.Sdk.Models.GraphTraversal;
using Speckle.Sdk.Models.Instances;
using Speckle.Sdk.Pipelines.Progress;
using DB = Autodesk.Revit.DB;
using Document = Autodesk.Revit.DB.Document;
namespace Speckle.Connectors.Revit.HostApp;
public sealed class RevitFamilyBaker : IDisposable
{
private readonly IConverterSettingsStore<RevitConversionSettings> _converterSettings;
private readonly RevitToHostCacheSingleton _cache;
private readonly ILogger<RevitFamilyBaker> _logger;
private readonly ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> _transformConverter;
private readonly RevitMaterialBaker _materialBaker;
private readonly FamilyGeometryBaker _familyGeometryBaker;
private readonly FamilyCategoryUtils _familyCategoryUtils;
private readonly FamilyTransformUtils _familyTransformUtils;
private string? _cachedTemplatePath;
private readonly Dictionary<string, string> _bakedFamilyPaths = [];
private readonly string _tempDirectory;
private static readonly char[] s_invalidChars = Path.GetInvalidFileNameChars();
public RevitFamilyBaker(
IConverterSettingsStore<RevitConversionSettings> converterSettings,
RevitToHostCacheSingleton cache,
ILogger<RevitFamilyBaker> logger,
ITypedConverter<(Matrix4x4 matrix, string units), DB.Transform> transformConverter,
RevitMaterialBaker materialBaker,
FamilyGeometryBaker familyGeometryBaker,
FamilyCategoryUtils familyCategoryUtils,
FamilyTransformUtils familyTransformUtils
)
{
_converterSettings = converterSettings;
_cache = cache;
_logger = logger;
_transformConverter = transformConverter;
_materialBaker = materialBaker;
_familyGeometryBaker = familyGeometryBaker;
_familyCategoryUtils = familyCategoryUtils;
_familyTransformUtils = familyTransformUtils;
_tempDirectory = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid().ToString("N")[..8]}");
Directory.CreateDirectory(_tempDirectory);
}
public (List<ReceiveConversionResult> results, List<string> createdElementIds) BakeInstances(
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents,
IReadOnlyDictionary<string, TraversalContext> speckleObjectLookup,
IReadOnlyCollection<RenderMaterialProxy> materialProxies,
IProgress<CardProgress> onOperationProgressed
)
{
var document = _converterSettings.Current.Document;
var results = new List<ReceiveConversionResult>();
var createdElementIds = new List<string>();
var (objectToMaterialMap, safeNameToProjectMatId) = BuildMaterialMaps(materialProxies);
var consumedIds = BuildConsumedIdsSet(instanceComponents, speckleObjectLookup);
var sortedComponents = SortComponentsForBaking(instanceComponents);
var count = 0;
foreach (var (_, component) in sortedComponents)
{
onOperationProgressed.Report(new("Creating families", (double)++count / sortedComponents.Count));
try
{
if (component is InstanceDefinitionProxy definitionProxy)
{
var categoryString = _familyCategoryUtils.ExtractCategoryForDefinition(
definitionProxy,
instanceComponents,
speckleObjectLookup
);
var result = CreateFamilyFromDefinition(
document,
definitionProxy,
speckleObjectLookup,
objectToMaterialMap,
safeNameToProjectMatId,
categoryString
);
if (result.HasValue)
{
results.Add(
new ReceiveConversionResult(Status.SUCCESS, definitionProxy, result.Value.family.Id.ToString(), "Family")
);
}
}
else if (component is InstanceProxy instanceProxy)
{
bool isConsumed =
(instanceProxy.id != null && consumedIds.Contains(instanceProxy.id))
|| (instanceProxy.applicationId != null && consumedIds.Contains(instanceProxy.applicationId));
if (isConsumed)
{
continue;
}
var instance = PlaceFamilyInstance(document, instanceProxy);
if (instance != null)
{
createdElementIds.Add(instance.UniqueId);
if (_familyTransformUtils.HasScaleOrSkew(instanceProxy.transform))
{
var warningEx = new SpeckleException(
"Block instance placed with its original position and rotation, but the unsupported scale/skew was dropped"
);
results.Add(
new ReceiveConversionResult(
Status.WARNING,
instanceProxy,
instance.UniqueId,
"FamilyInstance",
warningEx
)
);
}
else
{
results.Add(
new ReceiveConversionResult(Status.SUCCESS, instanceProxy, instance.UniqueId, "FamilyInstance")
);
}
}
}
}
catch (Exception ex) when (!ex.IsFatal())
{
string componentId = component switch
{
InstanceDefinitionProxy d => d.applicationId ?? d.id.NotNull(),
InstanceProxy i => i.applicationId ?? i.id.NotNull(),
_ => "unknown"
};
_logger.LogError(ex, "Failed to process instance component {ComponentId}", componentId);
if (component is Base b)
{
results.Add(new ReceiveConversionResult(Status.ERROR, b, null, null, ex));
}
}
}
return (results, createdElementIds);
}
private (
Dictionary<string, RenderMaterial> objectToMaterialMap,
Dictionary<string, ElementId> safeNameToProjectMatId
) BuildMaterialMaps(IReadOnlyCollection<RenderMaterialProxy> materialProxies)
{
Dictionary<string, RenderMaterial> objectToMaterialMap = new();
Dictionary<string, ElementId> safeNameToProjectMatId = new();
foreach (var proxy in materialProxies)
{
string matId = proxy.value.id.NotNullOrWhiteSpace();
string safeName = string.IsNullOrWhiteSpace(proxy.value.name) ? matId : proxy.value.name;
foreach (var objId in proxy.objects)
{
objectToMaterialMap[objId] = proxy.value;
}
if (proxy.objects.Count > 0)
{
foreach (var objId in proxy.objects)
{
if (_cache.MaterialsByObjectId.TryGetValue(objId, out var projMatId))
{
safeNameToProjectMatId[safeName] = projMatId;
break;
}
}
}
}
return (objectToMaterialMap, safeNameToProjectMatId);
}
private static HashSet<string> BuildConsumedIdsSet(
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents,
IReadOnlyDictionary<string, TraversalContext> speckleObjectLookup
)
{
var consumedIds = new HashSet<string>();
foreach (var (_, component) in instanceComponents)
{
if (component is InstanceDefinitionProxy definition)
{
foreach (var childId in definition.objects ?? Enumerable.Empty<string>())
{
consumedIds.Add(childId);
if (speckleObjectLookup.TryGetValue(childId, out var childTc))
{
var childObj = childTc.Current;
if (childObj.id != null)
{
consumedIds.Add(childObj.id);
}
if (childObj.applicationId != null)
{
consumedIds.Add(childObj.applicationId);
}
}
}
}
}
return consumedIds;
}
private static List<(Collection[] collectionPath, IInstanceComponent component)> SortComponentsForBaking(
ICollection<(Collection[] collectionPath, IInstanceComponent component)> instanceComponents
) =>
instanceComponents
.OrderByDescending(x => x.component.maxDepth)
.ThenBy(x => x.component is InstanceDefinitionProxy ? 0 : 1)
.ToList();
private (Family family, FamilySymbol symbol)? CreateFamilyFromDefinition(
Document document,
InstanceDefinitionProxy definitionProxy,
IReadOnlyDictionary<string, TraversalContext> objectLookup,
IReadOnlyDictionary<string, RenderMaterial> materialMap,
IReadOnlyDictionary<string, ElementId> safeNameToProjectMatId,
string? categoryString
)
{
var definitionId = definitionProxy.applicationId ?? definitionProxy.id.NotNull();
if (_cache.FamiliesByDefinitionId.TryGetValue(definitionId, out var existingFamily))
{
var existingSymbol = _cache.SymbolsByDefinitionId[definitionId];
return (existingFamily, existingSymbol);
}
var familyName = GetFamilyName(definitionProxy);
bool isNewFamily = false;
var family = FindFamilyByName(document, familyName);
if (family == null)
{
family = CreateFamily(document, familyName, definitionProxy, objectLookup, materialMap, categoryString);
isNewFamily = true;
}
if (family == null)
{
_logger.LogWarning("Failed to create family for definition {DefinitionId}", definitionId);
return null;
}
var symbolId = family.GetFamilySymbolIds().FirstOrDefault();
if (symbolId == null || symbolId == ElementId.InvalidElementId)
{
return null;
}
if (document.GetElement(symbolId) is not FamilySymbol symbol)
{
return null;
}
if (!symbol.IsActive)
{
symbol.Activate();
document.Regenerate();
}
if (isNewFamily)
{
FamilyMaterialManager.AssignProjectMaterialsToFamily(document, symbol, safeNameToProjectMatId);
}
_cache.FamiliesByDefinitionId[definitionId] = family;
_cache.SymbolsByDefinitionId[definitionId] = symbol;
return (family, symbol);
}
private Family? CreateFamily(
Document document,
string familyName,
InstanceDefinitionProxy definition,
IReadOnlyDictionary<string, TraversalContext> objectLookup,
IReadOnlyDictionary<string, RenderMaterial> materialMap,
string? categoryString
)
{
var templatePath = GetFamilyTemplatePath(document);
var famDoc = document.Application.NewFamilyDocument(templatePath);
var tempPath = Path.Combine(_tempDirectory, $"{familyName}.rfa");
try
{
using (var t = new Transaction(famDoc, "Populate Family"))
{
t.Start();
var materialManager = new FamilyMaterialManager(_materialBaker, _logger);
materialManager.SetupFamilyMaterials(famDoc, definition, objectLookup, materialMap);
_familyGeometryBaker.BakeFamilyGeometry(
famDoc,
definition,
objectLookup,
materialMap,
materialManager,
PlaceNestedInstance
);
SetFamilyWorkPlaneBased(famDoc, true);
_familyCategoryUtils.SetFamilyCategory(famDoc, categoryString);
t.Commit();
}
var saveOptions = new SaveAsOptions { OverwriteExistingFile = true };
famDoc.SaveAs(tempPath, saveOptions);
famDoc.Close(false);
var definitionId = definition.applicationId ?? definition.id.NotNull();
_bakedFamilyPaths[definitionId] = tempPath;
document.LoadFamily(tempPath, new FamilyLoadOptions(), out var loadedFamily);
return loadedFamily;
}
catch (Autodesk.Revit.Exceptions.ApplicationException ex)
{
_logger.LogError(ex, "Revit API error creating family {FamilyName}", familyName);
famDoc.Close(false);
SafeDelete(tempPath);
throw;
}
catch (IOException ex)
{
_logger.LogError(ex, "IO error creating family {FamilyName}", familyName);
famDoc.Close(false);
SafeDelete(tempPath);
throw;
}
}
private void PlaceNestedInstance(Document famDoc, InstanceProxy instanceProxy, FamilyMaterialManager? materialManager)
{
var childDefinitionId = instanceProxy.definitionId;
if (!_bakedFamilyPaths.TryGetValue(childDefinitionId, out var rfaPath) || !File.Exists(rfaPath))
{
return;
}
var familyName = Path.GetFileNameWithoutExtension(rfaPath);
Family? childFamily = FindFamilyByName(famDoc, familyName) ?? LoadFamilyWrapper(famDoc, rfaPath);
using var _ = childFamily;
if (childFamily == null)
{
return;
}
var symbolId = childFamily.GetFamilySymbolIds().FirstOrDefault();
if (symbolId == null || famDoc.GetElement(symbolId) is not FamilySymbol symbol)
{
return;
}
if (!symbol.IsActive)
{
symbol.Activate();
}
var instance = CreateAndPlaceFamilyInstance(famDoc, instanceProxy, symbol);
if (instance != null && materialManager != null)
{
foreach (Parameter childParam in symbol.Parameters)
{
if (childParam.Definition.Name.StartsWith("Material_") && childParam.StorageType == StorageType.ElementId)
{
string paramName = childParam.Definition.Name;
FamilyParameter? parentFamParam =
famDoc.FamilyManager.get_Parameter(paramName)
?? famDoc.FamilyManager.AddParameter(
paramName,
GroupTypeId.Materials,
SpecTypeId.Reference.Material,
false
);
if (famDoc.FamilyManager.CanElementParameterBeAssociated(childParam))
{
try
{
famDoc.FamilyManager.AssociateElementParameterToFamilyParameter(childParam, parentFamParam);
}
catch (Autodesk.Revit.Exceptions.ArgumentException ex)
{
_logger.LogWarning(ex, "Failed to associate material parameter {ParamName}", paramName);
}
}
}
}
}
}
private static Family? LoadFamilyWrapper(Document doc, string path)
{
doc.LoadFamily(path, new FamilyLoadOptions(), out var family);
return family;
}
private FamilyInstance? CreateAndPlaceFamilyInstance(Document doc, InstanceProxy instanceProxy, FamilySymbol symbol)
{
var isMirrored = _familyTransformUtils.GetMirrorState(instanceProxy.transform).X;
var hasScaleOrSkew = _familyTransformUtils.HasScaleOrSkew(instanceProxy.transform);
var cleanMatrix =
(hasScaleOrSkew || isMirrored)
? _familyTransformUtils.RemoveScaleAndSkew(instanceProxy.transform)
: instanceProxy.transform;
var revitTransform = _transformConverter.Convert((cleanMatrix, instanceProxy.units));
XYZ origin = revitTransform.Origin;
XYZ basisX = revitTransform.BasisX.Normalize();
XYZ basisY = revitTransform.BasisY.Normalize();
var plane = DB.Plane.CreateByOriginAndBasis(origin, basisX, basisY);
using var sketchPlane = SketchPlane.Create(doc, plane);
var creationData = new FamilyInstanceCreationData(
location: origin,
symbol: symbol,
host: sketchPlane,
level: null,
structuralType: StructuralType.NonStructural
);
ICollection<ElementId> ids = doc.IsFamilyDocument
? doc.FamilyCreate.NewFamilyInstances2([creationData])
: doc.Create.NewFamilyInstances2([creationData]);
if (ids.Count == 0 || doc.GetElement(ids.First()) is not FamilyInstance instance)
{
return null;
}
doc.Regenerate();
var mirrorState = _familyTransformUtils.GetMirrorState(instanceProxy.transform);
_familyTransformUtils.ApplyMirroring(doc, instance.Id, plane, mirrorState);
return instance;
}
private FamilyInstance? PlaceFamilyInstance(Document document, InstanceProxy instanceProxy)
{
var definitionId = instanceProxy.definitionId;
if (_cache.SymbolsByDefinitionId.TryGetValue(definitionId, out var symbol))
{
return CreateAndPlaceFamilyInstance(document, instanceProxy, symbol);
}
_logger.LogWarning("No family symbol found for definition {DefinitionId}", definitionId);
return null;
}
private static void SetFamilyWorkPlaneBased(Document famDoc, bool enabled)
{
var workPlaneBasedParam = famDoc.OwnerFamily.get_Parameter(BuiltInParameter.FAMILY_WORK_PLANE_BASED);
if (workPlaneBasedParam != null && !workPlaneBasedParam.IsReadOnly)
{
workPlaneBasedParam.Set(enabled ? 1 : 0);
}
}
private string GetFamilyTemplatePath(Document document)
{
if (_cachedTemplatePath != null)
{
return _cachedTemplatePath;
}
var version = document.Application.VersionNumber;
var isMetric = document.DisplayUnitSystem == DisplayUnit.METRIC;
var templateName = isMetric ? "Metric Generic Model.rft" : "Generic Model.rft";
var assemblyLocation = typeof(RevitFamilyBaker).Assembly.Location;
var assemblyDir =
Path.GetDirectoryName(assemblyLocation) ?? throw new ConversionException("Could not resolve assembly directory");
var templatePath = Path.Combine(assemblyDir, "Resources", "Templates", version, templateName);
if (!File.Exists(templatePath))
{
_logger.LogError("Revit Family Template missing. Searched path: {templatePath}", templatePath);
throw new ConversionException($"Could not find required family template: {templateName}");
}
_cachedTemplatePath = templatePath;
return templatePath;
}
private static string GetFamilyName(InstanceDefinitionProxy definitionProxy)
{
var baseName = definitionProxy.name;
if (string.IsNullOrWhiteSpace(baseName))
{
return "Unnamed_Block";
}
char[] buffer = baseName.ToCharArray();
bool changed = false;
for (int i = 0; i < buffer.Length; i++)
{
if (Array.IndexOf(s_invalidChars, buffer[i]) >= 0)
{
buffer[i] = '_';
changed = true;
}
}
var safeName = changed ? new string(buffer) : baseName;
// truncate to avoid MAX_PATH exceptions. 100 chars should be very safe.
if (safeName.Length > 100)
{
// Append a short hash of the definition ID to guarantee uniqueness after truncation
var shortId = definitionProxy.id?[..8] ?? Guid.NewGuid().ToString("N")[..8];
return $"{safeName[..90]}_{shortId}";
}
return safeName;
}
private static Family? FindFamilyByName(Document document, string familyName)
{
using var collector = new FilteredElementCollector(document);
return collector.OfClass(typeof(Family)).OfType<Family>().FirstOrDefault(f => f.Name == familyName);
}
private static void SafeDelete(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
}
public void Dispose()
{
_bakedFamilyPaths.Clear();
try
{
if (Directory.Exists(_tempDirectory))
{
Directory.Delete(_tempDirectory, true);
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
_logger.LogWarning(ex, "Failed to clean up temporary family directory at {TempDir}", _tempDirectory);
}
}
private sealed class FamilyLoadOptions : IFamilyLoadOptions
{
public bool OnFamilyFound(bool familyInUse, out bool overwriteParameterValues)
{
overwriteParameterValues = true;
return true;
}
public bool OnSharedFamilyFound(
Family sharedFamily,
bool familyInUse,
out FamilySource source,
out bool overwriteParameterValues
)
{
source = FamilySource.Family;
overwriteParameterValues = true;
return true;
}
}
}