7860c44f4e
* 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>
622 lines
20 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|