diff --git a/Revit_Adapter/Revit_Adapter.csproj b/Revit_Adapter/Revit_Adapter.csproj index 48d9b6839..622224a45 100644 --- a/Revit_Adapter/Revit_Adapter.csproj +++ b/Revit_Adapter/Revit_Adapter.csproj @@ -8,7 +8,7 @@ BHoM Copyright © https://github.com/BHoM BH.Adapter.Revit - 9.1.0.0 + 9.2.0.0 diff --git a/Revit_Core_Adapter/AdapterActions/Push.cs b/Revit_Core_Adapter/AdapterActions/Push.cs index 22e8eba2b..a71b7d660 100644 --- a/Revit_Core_Adapter/AdapterActions/Push.cs +++ b/Revit_Core_Adapter/AdapterActions/Push.cs @@ -27,6 +27,7 @@ using BH.oM.Adapters.Revit.Elements; using BH.oM.Base; using BH.Revit.Engine.Core; +using System; using System.Collections.Generic; using System.Linq; @@ -60,7 +61,7 @@ public override List Push(IEnumerable objects, string tag = "", BH.Engine.Base.Compute.RecordError("BHoM objects could not be removed because another transaction is open in Revit."); return new List(); } - + // If unset, set the pushType to AdapterSettings' value (base AdapterSettings default is FullCRUD). Disallow the unsupported PushTypes. if (pushType == PushType.AdapterDefault) pushType = PushType.UpdateOrCreateOnly; @@ -69,7 +70,7 @@ public override List Push(IEnumerable objects, string tag = "", BH.Engine.Base.Compute.RecordError("Full Push is currently not supported by Revit_Toolkit, please use Create, UpdateOnly or DeleteThenCreate instead."); return new List(); } - + // Set config RevitPushConfig pushConfig = actionConfig as RevitPushConfig; if (pushConfig == null) @@ -81,7 +82,7 @@ public override List Push(IEnumerable objects, string tag = "", // Suppress warnings if (UIControlledApplication != null && pushConfig.SuppressFailureMessages) UIControlledApplication.ControlledApplication.FailuresProcessing += ControlledApplication_FailuresProcessing; - + // Process the objects (verify they are valid; DeepClone them, wrap them, etc). IEnumerable objectsToPush = ProcessObjectsForPush(objects, pushConfig); // Note: default Push only supports IBHoMObjects. @@ -131,6 +132,7 @@ public override List Push(IEnumerable objects, string tag = "", { List distinctNames = group.Select(x => x.Name).Distinct().ToList(); if (distinctNames.Count > 1) + BH.Engine.Base.Compute.RecordWarning($"BHoM objects with names {string.Join(", ", distinctNames)} correspond to the same Revit assembly that has finally been named {group.Key.AssemblyTypeName}."); } } @@ -149,55 +151,85 @@ public override List Push(IEnumerable objects, string tag = "", private List PushToRevit(Document document, IEnumerable objects, PushType pushType, RevitPushConfig pushConfig, string transactionName) { + + SketchUpdateQueue.SketchUpdates.Clear(); + List pushed = new List(); - using (Transaction transaction = new Transaction(document, transactionName)) + + using (TransactionGroup tg = new TransactionGroup(document, transactionName)) { - transaction.Start(); + tg.Start(); - if (pushType == PushType.CreateOnly) - pushed = Create(objects, pushConfig); - else if (pushType == PushType.CreateNonExisting) + using (Transaction transaction = new Transaction(document, transactionName)) { - IEnumerable toCreate = objects.Where(x => x.Element(document) == null); - pushed = Create(toCreate, pushConfig); - } - else if (pushType == PushType.DeleteThenCreate) - { - List toCreate = new List(); - foreach (IBHoMObject obj in objects) + transaction.Start(); + + if (pushType == PushType.CreateOnly) + pushed = Create(objects, pushConfig); + else if (pushType == PushType.CreateNonExisting) { - Element element = obj.Element(document); - if (element == null || Delete(element.Id, document, false).Count() != 0) - toCreate.Add(obj); + IEnumerable toCreate = objects.Where(x => x.Element(document) == null); + pushed = Create(toCreate, pushConfig); } - - pushed = Create(toCreate, pushConfig); - } - else if (pushType == PushType.UpdateOnly) - { - foreach (IBHoMObject obj in objects) + else if (pushType == PushType.DeleteThenCreate) + { + List toCreate = new List(); + foreach (IBHoMObject obj in objects) + { + Element element = obj.Element(document); + if (element == null || Delete(element.Id, document, false).Count() != 0) + toCreate.Add(obj); + } + + pushed = Create(toCreate, pushConfig); + } + else if (pushType == PushType.UpdateOnly) { - Element element = obj.Element(document); - if (element != null && Update(element, obj, pushConfig)) - pushed.Add(obj); + foreach (IBHoMObject obj in objects) + { + Element element = obj.Element(document); + if (element != null && Update(element, obj, pushConfig)) + pushed.Add(obj); + } } + else if (pushType == PushType.UpdateOrCreateOnly) + { + List toCreate = new List(); + foreach (IBHoMObject obj in objects) + { + Element element = obj.Element(document); + if (element != null && Update(element, obj, pushConfig)) + pushed.Add(obj); + else if (element == null || Delete(element.Id, document, false).Count() != 0) + toCreate.Add(obj); + } + + pushed.AddRange(Create(toCreate, pushConfig)); + } + + transaction.Commit(); } - else if (pushType == PushType.UpdateOrCreateOnly) + + if (SketchUpdateQueue.SketchUpdates.Count > 0) { - List toCreate = new List(); - foreach (IBHoMObject obj in objects) + foreach (Action call in SketchUpdateQueue.SketchUpdates) { - Element element = obj.Element(document); - if (element != null && Update(element, obj, pushConfig)) - pushed.Add(obj); - else if (element == null || Delete(element.Id, document, false).Count() != 0) - toCreate.Add(obj); + try + { + call.Invoke(); + } + catch (Exception ex) + { + string errorMsg = $"Sketch update failed: {ex.Message}"; + if (ex.InnerException != null) + errorMsg += $" Inner: {ex.InnerException.Message}"; + + BH.Engine.Base.Compute.RecordError(errorMsg); + } } - - pushed.AddRange(Create(toCreate, pushConfig)); } - transaction.Commit(); + tg.Assimilate(); } return pushed; diff --git a/Revit_Core_Adapter/Revit_Core_Adapter.csproj b/Revit_Core_Adapter/Revit_Core_Adapter.csproj index e1658b744..e9e2d9f5f 100644 --- a/Revit_Core_Adapter/Revit_Core_Adapter.csproj +++ b/Revit_Core_Adapter/Revit_Core_Adapter.csproj @@ -7,7 +7,7 @@ BHoM Copyright © https://github.com/BHoM BH.Revit.Adapter.Core - 9.1.0.0 + 9.2.0.0 diff --git a/Revit_Core_Engine/Compute/GeneratePadFoundationType.cs b/Revit_Core_Engine/Compute/GeneratePadFoundationType.cs new file mode 100644 index 000000000..8c708ce62 --- /dev/null +++ b/Revit_Core_Engine/Compute/GeneratePadFoundationType.cs @@ -0,0 +1,269 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using Autodesk.Revit.DB; +using Autodesk.Revit.UI; +using BH.Engine.Adapters.Revit; +using BH.Engine.Geometry; +using BH.oM.Adapters.Revit.Settings; +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using BH.oM.Physical.Elements; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Extrusion = Autodesk.Revit.DB.Extrusion; + +namespace BH.Revit.Engine.Core +{ + public static partial class Compute + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Generates a Revit FamilySymbol for a BHoM PadFoundation by querying existing families in the document or creating a new one from a template file with parametric dimensions.")] + [Input("padFoundation", "BHoM pad foundation to generate the Revit profile for.")] + [Input("document", "Revit document, in which the family type will be created.")] + [Input("settings", "Settings to be used when generating the family type.")] + [Output("symbol", "Created Revit family type that represents the outline of the input BHoM pad foundation.")] + public static FamilySymbol GeneratePadFoundationType(this PadFoundation padFoundation, Document document, RevitSettings settings = null) + { + settings = settings.DefaultIfNull(); + + bool isRectangle; + Polyline outline = padFoundation?.PadFoundationOutline(); + if (outline != null) + isRectangle = outline.IsRectangular(settings.DistanceTolerance); + else + return null; + + if (isRectangle) + return GenerateRectangularType(padFoundation, document, settings); + else + return GenerateFreeformType(padFoundation, document, settings); + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static FamilySymbol GenerateRectangularType(PadFoundation padFoundation, Document document, RevitSettings settings) + { + // Check if family loaded to the document, if not, load it from resources path + string familyName = "StructuralFoundations_Pad-Rectangular"; + + Family family = new FilteredElementCollector(document).OfClass(typeof(Family)).FirstOrDefault(x => x.Name.EndsWith(familyName)) as Family; + if (family == null) + { + string path = Directory.GetFiles(m_FamilyDirectory, $"*{familyName}.rfa").FirstOrDefault(); + if (!File.Exists(path)) + return null; + + if (!document.LoadFamily(path, out family) || family == null) + return null; + } + + // Get dimensions of the pad foundation + (double, double, double) dimensions = padFoundation.RectangularDimensions(); + if (double.IsNaN(dimensions.Item1) || double.IsNaN(dimensions.Item2) || double.IsNaN(dimensions.Item3)) + return null; + return family.FindOrCreateTypeWithDimensions(dimensions.Item1, dimensions.Item2, dimensions.Item3); + } + + /***************************************************/ + private static FamilySymbol FindOrCreateTypeWithDimensions(this Family family, double width, double length, double thickness) + { + List symbols = family.GetFamilySymbolIds().Select(id => family.Document.GetElement(id) as FamilySymbol).Where(s => s != null).ToList(); + + long widthMm = (long)Math.Round(width * 1000.0); + long lengthMm = (long)Math.Round(length * 1000.0); + long depthMm = (long)Math.Round(thickness * 1000.0); + string typeName = $"{widthMm}x{lengthMm}x{depthMm}mm"; + + FamilySymbol result = symbols.FirstOrDefault(x => x?.Name == typeName); + if (result == null && symbols.Count != 0) + { + result = symbols.FirstOrDefault(x => !(new FilteredElementCollector(family.Document).WherePasses(new FamilyInstanceFilter(family.Document, x.Id)).Any())); + if (result != null) + result.Name = typeName; + else + result = symbols[0].Duplicate(typeName) as FamilySymbol; + result.SetParameter("Width", width); + result.SetParameter("Length", length); + result.SetParameter("Foundation Thickness", thickness); + } + result?.Activate(); + return result; + } + + /***************************************************/ + private static (double, double, double) RectangularDimensions(this PadFoundation padFoundation) + { + Polyline outline = padFoundation.PadFoundationOutline(); + double len1 = outline.ControlPoints[0].Distance(outline.ControlPoints[1]); + double len2 = outline.ControlPoints[1].Distance(outline.ControlPoints[2]); + double bhomLength = Math.Max(len1, len2); + double bhomWidth = Math.Min(len1, len2); + + return (bhomWidth, bhomLength, padFoundation.PadFoundationThickness()); + } + + /***************************************************/ + + private static FamilySymbol GenerateFreeformType(PadFoundation padFoundation, Document document, RevitSettings settings) + { + string prefix = "StructuralFoundations_Pad-Freeform_"; + + // Get the outline and check if valid + Polyline outline = padFoundation.PadFoundationOutline(); + if (outline?.IIsClosed() != true) + { + BH.Engine.Base.Compute.RecordError($"Pad foundation outline is invalid. BHoM_Guid: {padFoundation.BHoM_Guid}"); + return null; + } + + // Get the thickness and check if valid + double thickness = padFoundation.PadFoundationThickness(); + if (double.IsNaN(thickness)) + return null; + + // Orient the outline to origin + Polyline orientedOutline = outline.OrientToOrigin(); + if (orientedOutline == null) + return null; + + // Get all BHoM-generated freeform families in the document + List freeformFamilies = new FilteredElementCollector(document).OfClass(typeof(Family)).Cast() + .Where(x => Regex.IsMatch(x.Name, $"{prefix}\\d+$")).ToList(); + + // Try to find a family with matching outline, otherwise create a new one from template + Family family = freeformFamilies.FirstOrDefault(x => x.IsMatchingOutline(orientedOutline, settings)); + if (family == null) + { + List takenIndices = freeformFamilies.Select(x => Regex.Match(x.Name, $"{Regex.Escape(prefix)}(\\d+)$")).Select(x => int.Parse(x.Groups[1].Value)).ToList(); + int newIndex = takenIndices.Count > 0 ? takenIndices.Max() + 1 : 1; + family = GenerateFreeFormPadFamilyFromTemplate(document, orientedOutline, thickness, newIndex, padFoundation, settings); + } + + if (family == null) + return null; + + // Find or create the type with matching thickness + return family.FindOrCreateTypeWithThickness(thickness); + } + + /***************************************************/ + + private static Family GenerateFreeFormPadFamilyFromTemplate(this Document document, Polyline orientedOutline, double thickness, int index, PadFoundation padFoundation, RevitSettings settings = null) + { + string templateFamilyName = "StructuralFoundations_Pad-Freeform"; + string templatePath = Directory.GetFiles(m_FamilyDirectory, $"*{templateFamilyName}.rfa").FirstOrDefault(); + + Document familyDocument = new UIDocument(document).Application.Application.OpenDocumentFile(templatePath); + if (familyDocument == null) + return null; + + try + { + if (!ReplaceFreeFormExtrusion(familyDocument, orientedOutline, padFoundation)) + return null; + + return SaveAndLoadFamily(document, familyDocument, $"{Path.GetFileNameWithoutExtension(templatePath)}_{index}"); + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError($"Creation of a freeform Revit pad foundation failed with the following error: {ex.Message}"); + return null; + } + finally + { + familyDocument.Close(false); + } + } + + /***************************************************/ + private static double FreeformExtrusionDepth(PadFoundation padFoundation) + { + double depth = padFoundation.PadFoundationThickness(); + double h = double.IsNaN(depth) ? double.NaN : depth.FromSI(SpecTypeId.Length); + if (double.IsNaN(h) || h <= 1e-6) + h = 0.5.FromSI(SpecTypeId.Length); + return h; + } + + /***************************************************/ + private static bool ReplaceFreeFormExtrusion(Document familyDocument, Polyline orientedOutline, PadFoundation padFoundation) + { + try + { + Extrusion extrusion = new FilteredElementCollector(familyDocument).OfClass(typeof(Extrusion)).FirstOrDefault() as Extrusion; + CurveArrArray profile = new CurveArrArray(); + profile.Append(orientedOutline.ToRevitCurveArray()); + + using (Transaction t = new Transaction(familyDocument, "Update Freeform Pad Foundation Footprint")) + { + t.Start(); + familyDocument.FamilyCreate.NewExtrusion(true, profile, extrusion.Sketch.SketchPlane, -FreeformExtrusionDepth(padFoundation)); + familyDocument.Delete(extrusion.Id); + t.Commit(); + } + } + catch + { + return false; + } + + return true; + } + + /***************************************************/ + + private static FamilySymbol FindOrCreateTypeWithThickness(this Family family, double thickness) + { + List symbols = family.GetFamilySymbolIds().Select(id => family.Document.GetElement(id) as FamilySymbol).Where(s => s != null).ToList(); + if (symbols.Count == 0) + return null; + + string typeName = $"{(long)Math.Round(thickness * 1000.0)}mm"; + FamilySymbol result = symbols.FirstOrDefault(x => x?.Name == typeName); + if (result == null && symbols.Count != 0) + { + result = symbols.FirstOrDefault(x => !(new FilteredElementCollector(family.Document).WherePasses(new FamilyInstanceFilter(family.Document, x.Id)).Any())); + if (result != null) + result.Name = typeName; + else + result = symbols[0].Duplicate(typeName) as FamilySymbol; + + result.SetParameter("Foundation Thickness", thickness); + } + + result?.Activate(); + return result; + + /***************************************************/ + } + } +} diff --git a/Revit_Core_Engine/Compute/GenerateProfile.cs b/Revit_Core_Engine/Compute/GenerateProfile.cs index d9948f03d..19fafa165 100644 --- a/Revit_Core_Engine/Compute/GenerateProfile.cs +++ b/Revit_Core_Engine/Compute/GenerateProfile.cs @@ -60,7 +60,8 @@ public static FamilySymbol GenerateProfile(this IFramingElement element, Documen return null; } - string familyName = element.ProfileFamilyName(); + bool fromTemplate = element.HasTemplateFamily(); + string familyName = element.ProfileFamilyName(!fromTemplate); if (string.IsNullOrWhiteSpace(familyName)) { BH.Engine.Base.Compute.RecordError($"Creation of a Revit profile family failed because neither the BHoM section property nor its profile has a name. BHoM_Guid: {property.BHoM_Guid}"); @@ -69,46 +70,47 @@ public static FamilySymbol GenerateProfile(this IFramingElement element, Documen settings = settings.DefaultIfNull(); + FamilySymbol result; Family family = new FilteredElementCollector(document).OfClass(typeof(Family)).FirstOrDefault(x => x.Name == familyName) as Family; if (family != null) { List symbols = family.GetFamilySymbolIds().Select(x => document.GetElement(x) as FamilySymbol).Where(x => x != null).ToList(); - FamilySymbol result = symbols.FirstOrDefault(x => x?.Name == element.Property.Name); - if (result == null && symbols.Count != 0) + result = symbols.FirstOrDefault(x => x?.Name == element.Property.Name); + if (result != null) + return result; + + if (fromTemplate && symbols.Count != 0) { result = symbols[0].Duplicate(element.Property.Name) as FamilySymbol; result.Activate(); - property.Profile.ICopyDimensions(result, settings); + element.ICopyProfileDimensions(result, settings); + return result; } - - return result; } - else - { + + if (fromTemplate) family = document.GenerateFamilyFromTemplate(element, familyName, settings); - if (family == null) - { - family = document.GenerateFreeformFamily(element, familyName, settings); - if (family == null) - return null; + else + family = document.GenerateFreeformFamily(element, familyName, settings); - if (!(property.Profile is FreeFormProfile)) - BH.Engine.Base.Compute.RecordWarning($"Generation of profiles with shape {property.Profile.GetType().Name} is currently not fully supported - a freeform, dimensionless profile with a dedicated family has been created."); - } + if (family == null) + return null; - family.SetMaterialForModelBehaviour(property?.Material); + if (!fromTemplate && !(property.Profile is FreeFormProfile)) + BH.Engine.Base.Compute.RecordWarning($"Generation of profiles with shape {property.Profile.GetType().Name} is currently not fully supported - a freeform, dimensionless profile with a dedicated family has been created."); - FamilySymbol result = document.GetElement(family.GetFamilySymbolIds().FirstOrDefault()) as FamilySymbol; - if (result == null) - { - BH.Engine.Base.Compute.RecordWarning($"Generation of a Revit family representing the section property of a BHoM framing element failed due to an internal error. BHoM_Guid: {element.BHoM_Guid}"); - return null; - } + family.SetMaterialForModelBehaviour(property?.Material); - result.Activate(); - result.Name = element.ProfileTypeName(); - return result; + result = document.GetElement(family.GetFamilySymbolIds().FirstOrDefault()) as FamilySymbol; + if (result == null) + { + BH.Engine.Base.Compute.RecordWarning($"Generation of a Revit family representing the section property of a BHoM framing element failed due to an internal error. BHoM_Guid: {element.BHoM_Guid}"); + return null; } + + result.Activate(); + result.Name = element.ProfileTypeName(); + return result; } @@ -116,7 +118,7 @@ public static FamilySymbol GenerateProfile(this IFramingElement element, Documen /**** Private methods ****/ /***************************************************/ - private static Family SaveAndLoadFamily(Document document, Document familyDocument, string familyName, IFramingElement element, RevitSettings settings) + private static Family SaveAndLoadFamily(Document document, Document templateDocument, string familyName) { Family result = null; string tempFolder = Path.GetTempPath(); @@ -129,7 +131,7 @@ private static Family SaveAndLoadFamily(Document document, Document familyDocume SaveAsOptions saveOptions = new SaveAsOptions(); saveOptions.OverwriteExistingFile = true; - familyDocument.SaveAs(tempLocation, saveOptions); + templateDocument.SaveAs(tempLocation, saveOptions); } catch (Exception ex) { @@ -155,7 +157,7 @@ private static Family SaveAndLoadFamily(Document document, Document familyDocume private static Family GenerateFamilyFromTemplate(this Document document, IFramingElement element, string familyName, RevitSettings settings = null) { - string templateFamilyName = element.TemplateProfileFamilyName(); + string templateFamilyName = element.ITemplateProfileFamilyName(); if (string.IsNullOrWhiteSpace(templateFamilyName)) return null; @@ -174,7 +176,7 @@ private static Family GenerateFamilyFromTemplate(this Document document, IFramin t.Commit(); } - result = SaveAndLoadFamily(document, familyDocument, familyName, element, settings); + result = SaveAndLoadFamily(document, familyDocument, familyName); } catch (Exception ex) { @@ -223,7 +225,10 @@ private static Family GenerateFreeformFamily(this Document document, IFramingEle Family result = null; UIDocument uidoc = new UIDocument(document); - Document familyDocument = uidoc.Application.Application.OpenDocumentFile($"{m_FamilyDirectory}\\{element.IProfileFamilyNamePrefix()}FreeformProfile.rfa"); + + string familyTemplateNameMask = $"*{element.IProfileFamilyNamePrefix()}FreeformProfile.rfa"; + string filePath = Directory.GetFiles(m_FamilyDirectory, familyTemplateNameMask).FirstOrDefault(); + Document familyDocument = uidoc.Application.Application.OpenDocumentFile(filePath); try { @@ -273,7 +278,7 @@ private static Family GenerateFreeformFamily(this Document document, IFramingEle t.Commit(); } - result = SaveAndLoadFamily(document, familyDocument, familyName, element, settings); + result = SaveAndLoadFamily(document, familyDocument, familyName); } catch (Exception ex) { @@ -289,6 +294,14 @@ private static Family GenerateFreeformFamily(this Document document, IFramingEle /***************************************************/ + private static bool HasTemplateFamily(this IFramingElement element) + { + string templateFamilyName = element.ITemplateProfileFamilyName(); + return !string.IsNullOrWhiteSpace(templateFamilyName) && !templateFamilyName.Contains(m_FamilyFileNames[typeof(FreeFormProfile)]); + } + + /***************************************************/ + private static bool SetMaterialSubcategory(this Document familyDocument, BH.oM.Physical.Materials.Material material) { List structuralProperties = material?.Properties?.Where(x => x is BH.oM.Structure.MaterialFragments.IMaterialFragment)?.ToList(); @@ -373,7 +386,14 @@ private static void CopyProfileDimensions(this Pile element, FamilySymbol target return; } - CopyPileDimensions(sourceProfile as dynamic, targetSymbol, settings); + if (!(sourceProfile is CircleProfile circle)) + { + BH.Engine.Base.Compute.RecordError($"Could not copy profile dimensions because the BHoM framing element is not a pile with a circular profile. BHoM_Guid: {element.BHoM_Guid}"); + return; + } + + Parameter parameter = targetSymbol.ParametersMap.Cast().FirstOrDefault(x => x.Definition.Name.Contains("Diameter")); + parameter?.Set(Convert.FromSI(circle.Diameter, SpecTypeId.Length)); } /***************************************************/ @@ -387,7 +407,7 @@ private static void CopyProfileDimensions(this IFramingElement element, FamilySy return; } - CopyDimensions(sourceProfile as dynamic, targetSymbol, settings); + sourceProfile.ICopyDimensions(targetSymbol, settings); } /***************************************************/ @@ -594,14 +614,7 @@ private static void CopyDimensions(this GeneralisedFabricatedBoxProfile sourcePr /***************************************************/ - private static void CopyPileDimensions(this CircleProfile sourceProfile, FamilySymbol targetSymbol, RevitSettings settings = null) - { - targetSymbol.SetParameter("Diameter", sourceProfile.Diameter); - } - - /***************************************************/ - - private static string ProfileFamilyName(this IFramingElement element) + private static string ProfileFamilyName(this IFramingElement element, bool freeform) { string propertyName = element?.Property?.Name; string profileName = (element?.Property as ConstantFramingProperty)?.Profile?.Name; @@ -617,11 +630,16 @@ private static string ProfileFamilyName(this IFramingElement element) else { // Take template fam name and remove last section after underscore - string prefix = Regex.Replace(element.TemplateProfileFamilyName(), "_[^_]*$", ""); + string prefix = Regex.Replace(element.ITemplateProfileFamilyName(), "_[^_]*$", ""); // Add profile name - Regex pattern = new Regex(@"\d([\d\.\/\-xX ])*\d"); - return $"{prefix}_{pattern.Replace(name, "").Replace(" ", " ").Trim()}"; + if (!freeform) + { + Regex pattern = new Regex(@"\d([\d\.\/\-xX ])*\d"); + name = pattern.Replace(name, "").Replace(" ", " ").Trim(); + } + + return $"{prefix}_{name}"; } } @@ -643,6 +661,13 @@ private static string ProfileTypeName(this IFramingElement element) /***************************************************/ + private static string ITemplateProfileFamilyName(this IFramingElement element) + { + return TemplateProfileFamilyName(element as dynamic); + } + + /***************************************************/ + private static string TemplateProfileFamilyName(this IFramingElement element) { Type profileType = (element?.Property as ConstantFramingProperty)?.Profile?.GetType(); @@ -659,6 +684,28 @@ private static string TemplateProfileFamilyName(this IFramingElement element) /***************************************************/ + private static string TemplateProfileFamilyName(this Pile element) + { + Type profileType = (element?.Property as ConstantFramingProperty)?.Profile?.GetType(); + if (!m_FamilyFileNames.ContainsKey(profileType)) + return null; + + string profileName; + if (profileType == typeof(CircleProfile)) + profileName = m_FamilyFileNames[typeof(CircleProfile)]; + else + profileName = m_FamilyFileNames[typeof(FreeFormProfile)]; + + string name = element.IProfileFamilyNamePrefix() + profileName; + string path = Directory.GetFiles(m_FamilyDirectory, $"*{name}.rfa").FirstOrDefault(); + if (!string.IsNullOrEmpty(path)) + return Path.GetFileNameWithoutExtension(path); + else + return null; + } + + /***************************************************/ + private static string IProfileFamilyNamePrefix(this IFramingElement element) { return ProfileFamilyNamePrefix(element as dynamic); @@ -811,4 +858,3 @@ private static void CopyDimensions(this BH.oM.Spatial.ShapeProfiles.IProfile sou } - diff --git a/Revit_Core_Engine/Convert/Physical/ToRevit/FamilyInstance.cs b/Revit_Core_Engine/Convert/Physical/ToRevit/FamilyInstance.cs index e876bdb4f..167e8595f 100644 --- a/Revit_Core_Engine/Convert/Physical/ToRevit/FamilyInstance.cs +++ b/Revit_Core_Engine/Convert/Physical/ToRevit/FamilyInstance.cs @@ -221,6 +221,56 @@ public static FamilyInstance ToRevitFamilyInstance(this Pile framingElement, Doc return familyInstance; } + /***************************************************/ + [Description("Converts BH.oM.Physical.Elements.PadFoundation to a Revit FamilyInstance.")] + [Input("padFoundation", "BH.oM.Physical.Elements.PadFoundation to be converted.")] + [Input("document", "Revit document, in which the output of the convert will be created.")] + [Input("settings", "Revit adapter settings to be used while performing the convert.")] + [Input("refObjects", "Optional, a collection of objects already processed in the current adapter action, stored to avoid processing the same object more than once.")] + [Output("instance", "Revit FamilyInstance resulting from converting the input BH.oM.Physical.Elements.PadFoundation.")] + + public static FamilyInstance ToRevitFamilyInstance(this PadFoundation padFoundation, Document document, RevitSettings settings = null, Dictionary> refObjects = null) + { + if (padFoundation == null || document == null) + return null; + + FamilyInstance familyInstance = refObjects.GetValue(document, padFoundation.BHoM_Guid); + if (familyInstance != null) + return familyInstance; + + settings = settings.DefaultIfNull(); + + BH.oM.Geometry.Point origin = padFoundation.PadFoundationCentroid(); + if (origin == null) + { + BH.Engine.Base.Compute.RecordError($"PadFoundation boundary extraction failed or foundation is not rectangular. BHoM_Guid: {padFoundation.BHoM_Guid}"); + return null; + } + + XYZ placementPoint = origin.ToRevit(); + + Level level = document.LevelAbove(placementPoint.Z, settings); + if (level == null) + return null; + + FamilySymbol familySymbol = padFoundation.ElementType(document, settings) as FamilySymbol; + if (familySymbol == null) + { + Compute.ElementTypeNotFoundWarning(padFoundation); + return null; + } + + familySymbol.Activate(); + familyInstance = document.Create.NewFamilyInstance(placementPoint, familySymbol, level, StructuralType.Footing); + document.Regenerate(); + + familyInstance.CopyParameters(padFoundation, settings); + familyInstance.SetLocation(padFoundation, settings); + + refObjects.AddOrReplace(padFoundation, familyInstance); + return familyInstance; + } + /***************************************************/ [Description("Converts BH.oM.Physical.Elements.IFramingElement to a Revit FamilyInstance.")] @@ -349,10 +399,4 @@ public static FamilyInstance ToRevitFamilyInstance(this IFramingElement framingE /***************************************************/ } -} - - - - - - +} \ No newline at end of file diff --git a/Revit_Core_Engine/Convert/Physical/ToRevit/RoofBase.cs b/Revit_Core_Engine/Convert/Physical/ToRevit/RoofBase.cs index 26b267218..112c024e4 100644 --- a/Revit_Core_Engine/Convert/Physical/ToRevit/RoofBase.cs +++ b/Revit_Core_Engine/Convert/Physical/ToRevit/RoofBase.cs @@ -94,24 +94,69 @@ public static RoofBase ToRevitRoofBase(this oM.Physical.Elements.Roof roof, Docu if (controlPoints != null && controlPoints.Count > 2) { + try + { #if REVIT2022 || REVIT2023 || REVIT2024 || REVIT2025 - SlabShapeEditor slabShapeEditor = roofBase.SlabShapeEditor; -#else - SlabShapeEditor slabShapeEditor = roofBase.GetSlabShapeEditor(); -#endif - slabShapeEditor.ResetSlabShape(); + SlabShapeEditor slabShapeEditor = roofBase.SlabShapeEditor; + slabShapeEditor.ResetSlabShape(); - foreach (oM.Geometry.Point point in controlPoints) - { - if (Math.Abs(point.Z - plane.Origin.Z) > settings.DistanceTolerance) + foreach (oM.Geometry.Point point in controlPoints) { - XYZ xyz = point.ToRevit(); -#if REVIT2022 || REVIT2023 || REVIT2024 || REVIT2025 - slabShapeEditor.DrawPoint(xyz); + if (Math.Abs(point.Z - plane.Origin.Z) > settings.DistanceTolerance) + { + XYZ xyz = point.ToRevit(); + slabShapeEditor.DrawPoint(xyz); + } + } #else - slabShapeEditor.AddPoint(xyz); -#endif + FootPrintRoof fpr = roofBase as FootPrintRoof; + ModelCurveArrArray profiles = fpr.GetProfiles(); + + List curves = new List(); + foreach (ModelCurveArray array in profiles) + { + foreach (ModelCurve mc in array) + { + curves.Add(mc.GeometryCurve); + } } + + SlabShapeEditor slabShapeEditor = roofBase.GetSlabShapeEditor(); + + if (!slabShapeEditor.IsEnabled) + slabShapeEditor.Enable(); + + document.Regenerate(); + + foreach (oM.Geometry.Point point in controlPoints) + { + if (Math.Abs(point.Z - plane.Origin.Z) > settings.DistanceTolerance) + { + XYZ xyz = point.ToRevit(); + XYZ projected = new XYZ(xyz.X, xyz.Y, level.Elevation); + + Curve closest = null; + double minDist = double.MaxValue; + foreach (Curve crv in curves) + { + double dist = crv.Distance(projected); + if (dist < minDist) + { + minDist = dist; + closest = crv; + } + } + + XYZ onCurve = closest.Project(projected).XYZPoint; + XYZ onCurveOnFace = new XYZ(onCurve.X, onCurve.Y, xyz.Z); + slabShapeEditor.AddPoint(onCurveOnFace); + } + } +#endif + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError($"Failed to update roof shape {roofBase.Id.Value()}: {ex.Message}"); } } } diff --git a/Revit_Core_Engine/Convert/ToRevit.cs b/Revit_Core_Engine/Convert/ToRevit.cs index cd0357341..d46d343b5 100644 --- a/Revit_Core_Engine/Convert/ToRevit.cs +++ b/Revit_Core_Engine/Convert/ToRevit.cs @@ -273,6 +273,19 @@ public static Element ToRevit(this BH.oM.MEP.System.Pipe pipe, Document document /***************************************************/ + [Description("Converts BH.oM.Physical.Elements.PadFoundation to a Revit FamilyInstance.")] + [Input("foundation", "BH.oM.Physical.Elements.PadFoundation to be converted.")] + [Input("document", "Revit document, in which the output of the convert will be created.")] + [Input("settings", "Revit adapter settings to be used while performing the convert.")] + [Input("refObjects", "Optional, a collection of objects already processed in the current adapter action, stored to avoid processing the same object more than once.")] + [Output("instance", "Revit FamilyInstance resulting from converting the input BH.oM.Physical.Elements.PadFoundation.")] + public static Element ToRevit(this BH.oM.Physical.Elements.PadFoundation foundation, Document document, RevitSettings settings = null, Dictionary> refObjects = null) + { + return foundation.ToRevitFamilyInstance(document, settings, refObjects); + } + + /***************************************************/ + [Description("Converts BH.oM.MEP.System.MaterialFragments.PipeMaterial to a Revit PipeSegment.")] [Input("pipeMaterial", "BH.oM.MEP.System.MaterialFragments.PipeMaterial to be converted.")] [Input("document", "Revit document, in which the output of the convert will be created.")] diff --git a/Revit_Core_Engine/Files/Families/StructuralFoundations_FreeformProfile.rfa b/Revit_Core_Engine/Files/Families/StructuralFoundations_FreeformProfile.rfa index 9af197499..a3b763c24 100644 Binary files a/Revit_Core_Engine/Files/Families/StructuralFoundations_FreeformProfile.rfa and b/Revit_Core_Engine/Files/Families/StructuralFoundations_FreeformProfile.rfa differ diff --git a/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Freeform.rfa b/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Freeform.rfa new file mode 100644 index 000000000..425275255 Binary files /dev/null and b/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Freeform.rfa differ diff --git a/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Rectangular.rfa b/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Rectangular.rfa new file mode 100644 index 000000000..5a60fd2bc Binary files /dev/null and b/Revit_Core_Engine/Files/Families/StructuralFoundations_Pad-Rectangular.rfa differ diff --git a/Revit_Core_Engine/Modify/OrientToOrigin.cs b/Revit_Core_Engine/Modify/OrientToOrigin.cs new file mode 100644 index 000000000..9fc17731f --- /dev/null +++ b/Revit_Core_Engine/Modify/OrientToOrigin.cs @@ -0,0 +1,48 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Geometry; +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using System.ComponentModel; + +namespace BH.Revit.Engine.Core +{ + public static partial class Modify + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Transforms a given Polyline into a standardized orientation by translating it to the origin and rotating it in XY plane.")] + [Input("outline", "Polyline to be oriented to the origin.")] + [Output("orientedOutline", "Oriented polyline translated to the origin and rotated in XY plane.")] + public static Polyline OrientToOrigin(this Polyline outline) + { + (Vector translation, double rotation) = outline.TransformToOriginInXY(); + if (translation == null || double.IsNaN(rotation)) + return null; + + return outline.Translate(translation).Rotate(new Point(), Vector.ZAxis, rotation); + } + } +} diff --git a/Revit_Core_Engine/Modify/SetLocation.cs b/Revit_Core_Engine/Modify/SetLocation.cs index 7d23874e7..ffa2f7af1 100644 --- a/Revit_Core_Engine/Modify/SetLocation.cs +++ b/Revit_Core_Engine/Modify/SetLocation.cs @@ -21,6 +21,7 @@ */ using Autodesk.Revit.DB; +using BH.Engine.Adapters.Revit; using BH.Engine.Geometry; using BH.Engine.Spatial; using BH.oM.Adapters.Revit.Elements; @@ -344,6 +345,104 @@ public static bool SetLocation(this FamilyInstance element, Pile pile, RevitSett /***************************************************/ + [Description("Sets the location of a given Revit FamilyInstance based on a given BHoM PadFoundation.")] + [Input("element", "Revit FamilyInstance to be modified.")] + [Input("padFoundation", "BHoM PadFoundation acting as a source of information about the new location.")] + [Input("settings", "Revit adapter settings to be used while performing the operation.")] + [Output("success", "True if location of the input Revit FamilyInstance has been successfully set.")] + public static bool SetLocation(this FamilyInstance element, PadFoundation padFoundation, RevitSettings settings) + { + if (element == null || padFoundation == null) + return false; + + settings = settings.DefaultIfNull(); + + // Revit location point including coordinates and rotation + LocationPoint revitLocation = element.Location as LocationPoint; + if (revitLocation == null) + return false; + + Polyline bhomOutline = padFoundation.PadFoundationOutline(); + if (bhomOutline == null) + return false; + + // Transformation needed to bring BHoM outline to global XY + (Vector, double) bhomTransform = bhomOutline.TransformToOriginInXY(); + if (bhomTransform.Item1 == null || double.IsNaN(bhomTransform.Item2)) + return false; + + // Translation between the origin and BHoM centroid + XYZ bhomXY = (new BH.oM.Geometry.Point() - bhomTransform.Item1).ToRevit(); + + bool updated = false; + XYZ referencePoint = null; + foreach (Autodesk.Revit.DB.Face face in element.Faces(new Options(), settings)) + { + if (face is PlanarFace planarFace && Math.Abs(1 - planarFace.FaceNormal.DotProduct(XYZ.BasisZ)) <= BH.oM.Adapters.Revit.Tolerance.Angle) + { + referencePoint = planarFace.Centroid(); + break; + } + } + + if (referencePoint == null) + return false; + + XYZ deltaXY = new XYZ(bhomXY.X - referencePoint.X, bhomXY.Y - referencePoint.Y, 0); + + if (deltaXY.GetLength() > settings.DistanceTolerance) + { + ElementTransformUtils.MoveElement(element.Document, element.Id, deltaXY); + updated = true; + element.Document.Regenerate(); + } + + // Rotate if needed + double dRot = (-bhomTransform.Item2 - revitLocation.Rotation).NormalizeAngleDomain(); + if (Math.Abs(dRot) > settings.AngleTolerance) + { + Autodesk.Revit.DB.Line axis = Autodesk.Revit.DB.Line.CreateBound(bhomXY, bhomXY + XYZ.BasisZ); + ElementTransformUtils.RotateElement(element.Document, element.Id, axis, dRot); + updated = true; + element.Document.Regenerate(); + } + + BoundingBoxXYZ bbox = element.get_BoundingBox(null); + double topZ = bbox.Max.Z; + double bhomZ = -bhomTransform.Item1.Z.FromSI(SpecTypeId.Length); + double dz = bhomZ - topZ; + + // Move vertically if needed + if (Math.Abs(dz) > settings.DistanceTolerance) + { + Parameter offParam = element.get_Parameter(BuiltInParameter.FAMILY_BASE_LEVEL_OFFSET_PARAM); + if (offParam != null && offParam.HasValue && !offParam.IsReadOnly) + { + offParam.Set(offParam.AsDouble() + dz); + updated = true; + element.Document.Regenerate(); + } + else + { + try + { + XYZ p = revitLocation.Point; + revitLocation.Point = new XYZ(p.X, p.Y, p.Z + dz); + updated = true; + element.Document.Regenerate(); + } + catch (Exception) + { + BH.Engine.Base.Compute.RecordWarning($"Pad foundation vertical placement could not be adjusted (offset parameter missing or location read-only). ElementId: {element.Id.Value()}"); + } + } + } + + return updated; + } + + /***************************************************/ + [Description("Sets the location of a given Revit Space based on a given BHoM Space.")] [Input("revitSpace", "Revit Space to be modified.")] [Input("bHoMSpace", "BHoM Space acting as a source of information about the new location.")] @@ -446,6 +545,18 @@ public static bool SetLocation(this HostObject element, BH.oM.Physical.Elements. return false; } + /***************************************************/ + + [Description("Sets the location of a given Revit Floor based on a given BHoM Floor.")] + [Input("element", "Revit Floor to be modified.")] + [Input("bHoMObject", "BHoM Floor acting as a source of information about the new location.")] + [Input("settings", "Revit adapter settings to be used while performing the operation.")] + [Output("success", "True if location of the input Revit Floor has been successfully set.")] + public static bool SetLocation(this Autodesk.Revit.DB.Floor element, BH.oM.Physical.Elements.Floor bHoMObject, RevitSettings settings) + { + return element.SetLocation(bHoMObject.Location, settings); + } + /***************************************************/ /**** Fallback Methods ****/ @@ -494,6 +605,227 @@ public static bool ISetLocation(this Element element, IBHoMObject bHoMObject, Re /**** Private Methods ****/ /***************************************************/ + private static bool SetLocation(this Autodesk.Revit.DB.Floor element, BH.oM.Geometry.ISurface location, RevitSettings settings) + { + PlanarSurface ps = location as PlanarSurface; + if (ps == null) + { + BH.Engine.Base.Compute.RecordWarning("Floor location must be a PlanarSurface"); + return false; + } + + if (ps.InternalBoundaries != null && ps.InternalBoundaries.Count > 0) + BH.Engine.Base.Compute.RecordWarning($"Floor has openings which will be ignored during sketch update. ElementId: {element.Id.Value()}"); + + Document doc = element.Document; + ElementId floorId = element.Id; + Sketch sketch = new FilteredElementCollector(doc).OfClass(typeof(Sketch)).Cast().FirstOrDefault(s => s.OwnerId == floorId); + if (sketch?.Id == null || sketch.SketchPlane == null) + { + BH.Engine.Base.Compute.RecordError($"Floor sketch not found. ElementId: {element.Id.Value()}"); + return false; + } + + Level level = doc.GetElement(element.LevelId) as Level; + BH.oM.Geometry.Plane slabPlane = ps.FitPlane(); + + double floorThickness = 0; + FloorType floorType = doc.GetElement(element.GetTypeId()) as FloorType; + if (floorType?.GetCompoundStructure() != null) + floorThickness = floorType.GetCompoundStructure().GetWidth(); + + double bottomElevation = BH.Engine.Geometry.Query.IBounds(ps).Min.Z; + oM.Geometry.Plane sketchPlaneBhom = new oM.Geometry.Plane { Origin = new BH.oM.Geometry.Point { Z = bottomElevation }, Normal = Vector.ZAxis }; + ICurve curve = ps.ExternalBoundary.IProject(sketchPlaneBhom); + + bool isFlat = 1 - Math.Abs(Vector.ZAxis.DotProduct(slabPlane.Normal)) <= settings.AngleTolerance; + + double newOffset; + double tan = 0; + Autodesk.Revit.DB.Line slopeLineForCreate = null; + + if (isFlat) + { + newOffset = slabPlane.Origin.Z.FromSI(SpecTypeId.Length) - level.ProjectElevation; + } + else + { + Vector normal = slabPlane.Normal; + if (normal.Z < 0) + normal = -slabPlane.Normal; + + double angle = normal.Angle(Vector.ZAxis); + tan = Math.Tan(angle); + + XYZ dir = normal.Project(oM.Geometry.Plane.XY).ToRevit().Normalize(); + BH.oM.Geometry.Line ln = slabPlane.PlaneIntersection(sketchPlaneBhom); + XYZ start = ln.ClosestPoint(curve.IStartPoint(), true).ToRevit(); + + CurveLoop outlineForIntersection = curve.ToRevitCurveLoop(); + XYZ centroid = curve.ICentroid().ToRevit(); + + double extensionLength = 10000; + Autodesk.Revit.DB.Line slopeThroughCentroid = Autodesk.Revit.DB.Line.CreateBound(centroid - dir * extensionLength, centroid + dir * extensionLength); + + List edgePoints = slopeThroughCentroid.Intersections(outlineForIntersection); + if (edgePoints != null && edgePoints.Count >= 2) + { + edgePoints.Sort((a, b) => a.DotProduct(dir).CompareTo(b.DotProduct(dir))); + slopeLineForCreate = Autodesk.Revit.DB.Line.CreateBound(edgePoints.First(), edgePoints.Last()); + } + + double baseOffset = ln.Start.Z.FromSI(SpecTypeId.Length) - level.ProjectElevation; + + if (slopeLineForCreate != null) + { + XYZ slopeTail = slopeLineForCreate.GetEndPoint(0); + double signedDistFromLn = (slopeTail - start).DotProduct(dir); + baseOffset -= signedDistFromLn * tan; + } + + newOffset = baseOffset; + } + + CurveLoop newOutline; + BH.oM.Geometry.ICurve projectedBoundary; + if (isFlat) + { + Autodesk.Revit.DB.Plane revitPlane = sketch.SketchPlane.GetPlane(); + BH.oM.Geometry.Plane revitSketchBhomPlane = BH.Engine.Geometry.Create.Plane(revitPlane.Origin.PointFromRevit(), revitPlane.Normal.VectorFromRevit()); + projectedBoundary = ps.ExternalBoundary.IProject(revitSketchBhomPlane); + newOutline = projectedBoundary.ToRevitCurveLoop(); + } + else + { + projectedBoundary = curve; + newOutline = curve.ToRevitCurveLoop(); + } + + if (HasFloorChanged(element, sketch, newOutline, newOffset, settings)) + { + ElementId sketchId = sketch.Id; + ElementId floorTypeId = element.GetTypeId(); + ElementId levelId = element.LevelId; + + Autodesk.Revit.DB.Line slopeLine = slopeLineForCreate; + double getTan = tan; + double getOffset = newOffset; + CurveLoop getOutline = newOutline; + + SketchUpdateQueue.SketchUpdates.Enqueue(() => + { + Autodesk.Revit.DB.Floor floor = doc.GetElement(floorId) as Autodesk.Revit.DB.Floor; + if (floor == null || !floor.IsValidObject) + return; + + try + { + if (!isFlat) + { + // recreate floor + using (Transaction tSlope = new Transaction(doc, "Recreate sloped floor")) + { + tSlope.Start(); + doc.Delete(floor.Id); + doc.Regenerate(); + Autodesk.Revit.DB.Floor newFloor = Autodesk.Revit.DB.Floor.Create(doc, new List { getOutline }, floorTypeId, levelId, true, slopeLine, -getTan); + if (newFloor != null) + { + newFloor.SetParameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM, getOffset, false); + doc.Regenerate(); + } + tSlope.Commit(); + } + } + else + { + Sketch floorSketch = doc.GetElement(sketchId) as Sketch; + + UpdateSketchOutline(doc, sketchId, getOutline); + + using (Transaction tOffset = new Transaction(doc, "Update floor offset")) + { + tOffset.Start(); + floor.SetParameter(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM, getOffset, false); + doc.Regenerate(); + tOffset.Commit(); + } + } + } + catch (Exception ex) + { + BH.Engine.Base.Compute.RecordError($"Failed to update floor sketch for ElementId {floorId.Value()}: {ex.Message}"); + return; + } + }); + } + + return true; + } + + /***************************************************/ + + private static void UpdateSketchOutline(Document doc, ElementId sketchId, CurveLoop newOutline) + { + using (SketchEditScope ses = new SketchEditScope(doc, "Update floor sketch")) + { + ses.Start(sketchId); + using (Transaction t = new Transaction(doc, "Modify sketch profile")) + { + t.Start(); + Sketch currentSketch = doc.GetElement(sketchId) as Sketch; + if (currentSketch != null) + { + IList existingElements = currentSketch.GetAllElements(); + if (existingElements != null && existingElements.Count > 0) + doc.Delete(existingElements); + + SketchPlane sketchPlane = currentSketch.SketchPlane; + if (sketchPlane != null) + foreach (Curve curve in newOutline) + doc.Create.NewModelCurve(curve, sketchPlane); + } + t.Commit(); + } + ses.Commit(new SketchUpdateFailurePreprocessor()); + } + } + + /***************************************************/ + + private static bool HasFloorChanged(Autodesk.Revit.DB.Floor floor, Sketch sketch, CurveLoop newOutline, double newOffset, RevitSettings settings) + { + // newOffset is in Revit internal units (feet), so read without SI conversion + double currentOffset = floor.LookupParameterDouble(BuiltInParameter.FLOOR_HEIGHTABOVELEVEL_PARAM, false); + if (Math.Abs(currentOffset - newOffset) > settings.DistanceTolerance) + return true; + + IList sketchElements = sketch.GetAllElements(); + if (sketchElements == null || sketchElements.Count == 0) + return true; + + List currentCurves = new List(); + foreach (ElementId elemId in sketchElements) + { + ModelCurve modelCurve = floor.Document.GetElement(elemId) as ModelCurve; + if (modelCurve != null && modelCurve.GeometryCurve != null && !(modelCurve.GeometryCurve is Autodesk.Revit.DB.Line && modelCurve.GeometryCurve.Length < 1.1)) + currentCurves.Add(modelCurve.GeometryCurve); + } + + if (currentCurves.Count != newOutline.Count()) + return true; + + for (int i = 0; i < currentCurves.Count; i++) + { + if (!currentCurves[i].IsSimilar(newOutline.ElementAt(i), settings)) + return true; + } + + return false; + } + + /***************************************************/ + private static bool SetLocation(this FamilyInstance element, BH.oM.Geometry.Point location, Basis orientation, RevitSettings settings) { LocationPoint elementLocation = element.Location as LocationPoint; @@ -562,6 +894,7 @@ private static bool SetLocation(this FamilyInstance element, BH.oM.Geometry.Poin newLocation = linkTransform.OfPoint(newLocation); if (ir.Distance > settings.DistanceTolerance) + BH.Engine.Base.Compute.RecordWarning($"The location point used on update of a family instance has been snapped to its host face. ElementId: {element.Id.Value()}"); } } @@ -592,6 +925,7 @@ private static bool SetLocation(this FamilyInstance element, BH.oM.Geometry.Poin } if (1 - Math.Abs(revitNormal.DotProduct(bHoMNormal)) > settings.AngleTolerance) + BH.Engine.Base.Compute.RecordWarning($"The orientation applied to the family instance on update has different normal than the original one. Only in-plane rotation has been applied, the orientation out of plane has been ignored. ElementId: {element.Id.Value()}"); double angle = transform.BasisX.AngleOnPlaneTo(newX, revitNormal); @@ -605,9 +939,8 @@ private static bool SetLocation(this FamilyInstance element, BH.oM.Geometry.Poin return success; } - + /***************************************************/ - private static bool UpdateRotationOfVerticalElement(this FamilyInstance element, IFramingElement bhomElement, RevitSettings settings) { bool updated = false; @@ -637,7 +970,6 @@ private static bool UpdateRotationOfVerticalElement(this FamilyInstance element, return updated; } - /***************************************************/ /**** Private collections ****/ /***************************************************/ diff --git a/Revit_Core_Engine/Modify/Update.cs b/Revit_Core_Engine/Modify/Update.cs index 946ed22c6..04b1709fe 100644 --- a/Revit_Core_Engine/Modify/Update.cs +++ b/Revit_Core_Engine/Modify/Update.cs @@ -24,12 +24,14 @@ using Autodesk.Revit.DB.Plumbing; using BH.Engine.Adapters.Revit; using BH.Engine.Base; +using BH.Engine.Geometry; using BH.oM.Adapters.Revit.Elements; using BH.oM.Adapters.Revit.Settings; using BH.oM.Base; using BH.oM.Base.Attributes; using BH.oM.Geometry; using BH.oM.MEP.System.MaterialFragments; +using BH.oM.Physical.Elements; using System; using System.Collections.Generic; using System.ComponentModel; @@ -114,6 +116,63 @@ public static bool Update(this Element element, IBHoMObject bHoMObject, RevitSet /***************************************************/ + [Description("Updates a Revit pad foundation FamilyInstance from the BHoM PadFoundation using the standard element update.")] + [Input("element", "Revit FamilyInstance representing the pad foundation to update.")] + [Input("bHoMObject", "BHoM PadFoundation whose properties (and optionally location) should be applied to the Revit instance.")] + [Input("settings", "Revit adapter settings used for the underlying element update.")] + [Input("setLocationOnUpdate", "If false, only parameters and properties are updated; if true, the instance location is updated as well.")] + [Output("success", "True if the underlying Element.Update succeeded; dimension mismatch checks only emit warnings and do not change this value.")] + public static bool Update(this FamilyInstance element, PadFoundation bHoMObject, RevitSettings settings, bool setLocationOnUpdate) + { + settings = settings.DefaultIfNull(); + + // Base update + bool result = ((Element)element).Update((IBHoMObject)bHoMObject, settings, setLocationOnUpdate); + + FamilySymbol symbol = element.Document.GetElement(element.GetTypeId()) as FamilySymbol; + + // Check if outlines match after setting the type (location & orientation irrelevant) + Polyline outline = bHoMObject.PadFoundationOutline(); + bool isRectangle = outline.IsRectangular(settings.DistanceTolerance); + bool matchingOutline; + if (isRectangle) + { + double len1 = outline.ControlPoints[0].Distance(outline.ControlPoints[1]); + double len2 = outline.ControlPoints[1].Distance(outline.ControlPoints[2]); + double bhomLength = Math.Max(len1, len2); + double bhomWidth = Math.Min(len1, len2); + + double revitLength = symbol.LookupParameterDouble("Length"); + double revitWidth = symbol.LookupParameterDouble("Width"); + + matchingOutline = Math.Abs(bhomLength - revitLength) <= settings.DistanceTolerance && + Math.Abs(bhomWidth - revitWidth) <= settings.DistanceTolerance; + } + else + matchingOutline = symbol.Family.IsMatchingOutline(outline.OrientToOrigin(), settings); + + if (!matchingOutline) + BH.Engine.Base.Compute.RecordWarning($"Pad outline had not been updated successfully, there is a mismatch between BHoM and Revit dimensions. ElementId {element.Id.Value()}"); + + // Check if thickness matches after setting the type + bool matchingThickness; + double bhomThickness = bHoMObject.PadFoundationThickness(); + if (!double.IsNaN(bhomThickness)) + { + double revitThickness = symbol.LookupParameterDouble("Foundation Thickness"); + matchingThickness = Math.Abs(bhomThickness - revitThickness) <= settings.DistanceTolerance; + } + else + matchingThickness = false; + + if (!matchingThickness) + BH.Engine.Base.Compute.RecordWarning($"Pad thickness had not been updated successfully, there is a mismatch between BHoM and Revit dimensions. ElementId {element.Id.Value()}"); + + return result; + } + + /***************************************************/ + [Description("Updates the existing Revit FamilyInstance based on the given BHoM builders work Opening.")] [Input("element", "Revit FamilyInstance to be updated.")] [Input("bHoMObject", "BHoM builders work Opening, based on which the Revit element will be updated.")] diff --git a/Revit_Core_Engine/Objects/SketchUpdateFailurePreprocessor.cs b/Revit_Core_Engine/Objects/SketchUpdateFailurePreprocessor.cs new file mode 100644 index 000000000..30edec6b8 --- /dev/null +++ b/Revit_Core_Engine/Objects/SketchUpdateFailurePreprocessor.cs @@ -0,0 +1,45 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using Autodesk.Revit.DB; +using BH.oM.Base.Attributes; +using System.ComponentModel; + +namespace BH.Revit.Engine.Core +{ + internal class SketchUpdateFailurePreprocessor : IFailuresPreprocessor + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Preprocesses failures that occur during sketch updates, allowing the operation to continue despite warnings or errors.")] + [Input("failuresAccessor", "Revit failures accessor object containing failure messages from the sketch update operation.")] + [Output("failureProcessingResult", "The result of the failure processing, indicating whether to continue with the operation.")] + public FailureProcessingResult PreprocessFailures(FailuresAccessor failuresAccessor) + { + return FailureProcessingResult.Continue; + } + + /***************************************************/ + } +} diff --git a/Revit_Core_Engine/Objects/SketchUpdateQueue.cs b/Revit_Core_Engine/Objects/SketchUpdateQueue.cs new file mode 100644 index 000000000..3ed034bb9 --- /dev/null +++ b/Revit_Core_Engine/Objects/SketchUpdateQueue.cs @@ -0,0 +1,40 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace BH.Revit.Engine.Core +{ + public static class SketchUpdateQueue + { + /***************************************************/ + /**** Public Properties ****/ + /***************************************************/ + + [Description("Queue containing actions to update floor sketches. These updates are deferred until after the main push transaction to avoid conflicts with sketch editing.")] + public static Queue SketchUpdates { get; } = new Queue(); + + /***************************************************/ + } +} diff --git a/Revit_Core_Engine/Query/ElementType.cs b/Revit_Core_Engine/Query/ElementType.cs index 636da66b8..48be1da8b 100644 --- a/Revit_Core_Engine/Query/ElementType.cs +++ b/Revit_Core_Engine/Query/ElementType.cs @@ -78,6 +78,27 @@ public static FamilySymbol ElementType(this BH.oM.Physical.Elements.IFramingElem /***************************************************/ + [Description("Returns the Revit FamilySymbol to use when converting a BHoM PadFoundation. Orthogonal rectangular plans resolve by BHoM family/type name (document or library), then fall back to generation; non-rectangular plans use generation from geometry.")] + [Input("padFoundation", "BHoM pad foundation to find or generate a correspondent Revit family type for.")] + [Input("document", "Revit document to search for the element type or host generated types.")] + [Input("settings", "Revit adapter settings to be used while performing the query.")] + [Output("familySymbol", "Revit FamilySymbol to be used when converting the input pad foundation to Revit.")] + public static FamilySymbol ElementType(this BH.oM.Physical.Elements.PadFoundation padFoundation, Document document, RevitSettings settings = null) + { + FamilySymbol result = padFoundation.GeneratePadFoundationType(document, settings); + padFoundation.FamilyAndTypeNames(out string familyName, out string familyTypeName); + + if (!string.IsNullOrWhiteSpace(familyName) && result.FamilyName != familyName) + BH.Engine.Base.Compute.RecordWarning($"BHoM PadFoundation's name does not match Revit family name derived from its geometry. BHoM_Guid: {padFoundation.BHoM_Guid}"); + + if (!string.IsNullOrWhiteSpace(familyTypeName) && result.Name != familyTypeName) + BH.Engine.Base.Compute.RecordWarning($"BHoM PadFoundation's name does not match Revit type name derived from its geometry. BHoM_Guid: {padFoundation.BHoM_Guid}"); + + return result; + } + + /***************************************************/ + [Description("Returns the Revit element type to be used when converting a given BHoM IInstance to Revit.")] [Input("instance", "BHoM IInstance to find a correspondent Revit element type for.")] [Input("document", "Revit document to parse in search for the element type.")] diff --git a/Revit_Core_Engine/Query/IsMatchingOutline.cs b/Revit_Core_Engine/Query/IsMatchingOutline.cs new file mode 100644 index 000000000..b0565f417 --- /dev/null +++ b/Revit_Core_Engine/Query/IsMatchingOutline.cs @@ -0,0 +1,122 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using Autodesk.Revit.DB; +using BH.Engine.Geometry; +using BH.oM.Adapters.Revit.Settings; +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Revit.Engine.Core +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Checks whether the given BHoM Polyline outline matches the outline of the padFoundationFamily freeform extrusion in Revit.")] + [Input("padFoundationFamily", "Revit padFoundationFamily whose extrusion outline should be compared.")] + [Input("orientedOutline", "BHoM polyline outline to match against the padFoundationFamily outline.")] + [Input("settings", "Revit adapter settings providing distance tolerance for the matching.")] + [Output("matches", "True if the padFoundationFamily extrusion outline matches the input outline.")] + public static bool IsMatchingOutline(this Family padFoundationFamily, Polyline orientedOutline, RevitSettings settings) + { + double tol = settings.DistanceTolerance; + try + { + List bhomEdges = orientedOutline.SubParts().Where(x => x != null && x.Length() > tol).ToList(); + List revitEdges = padFoundationFamily.ExtrusionEdges(settings); + + if (bhomEdges.Count != revitEdges.Count) + return false; + + for (int i = 0; i < bhomEdges.Count; i++) + { + BH.oM.Geometry.Line bhomEdge = bhomEdges[i]; + bool edgeMatch = false; + foreach (oM.Geometry.Line revitEdge in revitEdges) + { + oM.Geometry.Point bhomStart = bhomEdge.Start; + oM.Geometry.Point bhomEnd = bhomEdge.End; + oM.Geometry.Point revitStart = revitEdge.Start; + oM.Geometry.Point revitEnd = revitEdge.End; + if ((bhomStart.Distance(revitStart) <= tol && bhomEnd.Distance(revitEnd) <= tol) + || (bhomStart.Distance(revitEnd) <= tol && bhomEnd.Distance(revitStart) <= tol)) + { + edgeMatch = true; + break; + } + } + + if (!edgeMatch) + return false; + } + + return true; + } + catch + { + return false; + } + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static List ExtrusionEdges(this Family family, RevitSettings settings) + { + ISet symbolIds = family.GetFamilySymbolIds(); + if (symbolIds == null || symbolIds.Count == 0) + return null; + + ElementType type = family.Document.GetElement(symbolIds.First()) as ElementType; + + Options options = new Options() + { + ComputeReferences = false, + DetailLevel = Autodesk.Revit.DB.ViewDetailLevel.Medium, + IncludeNonVisibleObjects = false + }; + + List faces = type.Faces(options); + List tops = faces.OfType().Where(x => x.FaceNormal.IsAlmostEqualTo(XYZ.BasisZ)).ToList(); + if (tops == null || tops.Count != 1) + return null; + + IList outlines = tops[0].GetEdgesAsCurveLoops(); + if (outlines == null || outlines.Count != 1) + return null; + + List edges = outlines[0].FromRevit().SubParts(); + List lines = edges.OfType().ToList(); + if (edges.Count != lines.Count) + return null; + + return lines.Where(x => x.Length() > settings.DistanceTolerance).Select(x => x.Project(new oM.Geometry.Plane())).ToList(); + } + } +} diff --git a/Revit_Core_Engine/Query/IsValid.cs b/Revit_Core_Engine/Query/IsValid.cs new file mode 100644 index 000000000..d8cb08702 --- /dev/null +++ b/Revit_Core_Engine/Query/IsValid.cs @@ -0,0 +1,50 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using Autodesk.Revit.DB; +using BH.oM.Base.Attributes; +using System.ComponentModel; + +namespace BH.Revit.Engine.Core +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Checks whether the given XYZ point is valid (not null and does not contain NaN or Infinity values).")] + [Input("point", "XYZ point to be checked for validity.")] + [Output("isValid", "True if the input XYZ point is valid (not null and all coordinates are finite numbers), otherwise false.")] + public static bool IsValid(this XYZ point) + { + if (point == null) + return false; + + return !double.IsNaN(point.X) && !double.IsInfinity(point.X) && + !double.IsNaN(point.Y) && !double.IsInfinity(point.Y) && + !double.IsNaN(point.Z) && !double.IsInfinity(point.Z); + } + + /***************************************************/ + } +} diff --git a/Revit_Core_Engine/Query/PadFoundation.cs b/Revit_Core_Engine/Query/PadFoundation.cs new file mode 100644 index 000000000..e102a4429 --- /dev/null +++ b/Revit_Core_Engine/Query/PadFoundation.cs @@ -0,0 +1,87 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Geometry; +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using BH.oM.Physical.Elements; +using System.ComponentModel; +using System.Linq; + +namespace BH.Revit.Engine.Core +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Extracts the outer rectangular boundary of a PadFoundation as a Polyline.")] + [Input("element", "PadFoundation element whose boundary should be extracted.")] + [Output("outline", "Polyline representing the PadFoundation external boundary.")] + public static Polyline PadFoundationOutline(this PadFoundation element) + { + ICurve outline = element?.Location?.ExternalBoundary; + if (outline == null) + return null; + + if (element.Location.InternalBoundaries.Count != 0) + BH.Engine.Base.Compute.RecordWarning($"PadFoundation with internal boundaries are currently not supported, holes were skipped. BHoM_Guid: {element.BHoM_Guid}"); + + if (outline.ISubParts().Any(x => !(x is Line))) + { + BH.Engine.Base.Compute.RecordError($"PadFoundation boundary contains non-linear curve segments. Only linear segments are currently supported. BHoM_Guid: {element.BHoM_Guid}"); + return null; + } + + return outline.IToPolyline(); + } + + /***************************************************/ + + [Description("Computes the total thickness (sum of construction layers) of a PadFoundation.")] + [Input("element", "PadFoundation element whose thickness should be computed.")] + [Output("thickness", "Total thickness of all construction layers.")] + public static double PadFoundationThickness(this PadFoundation element) + { + oM.Physical.Constructions.Construction construction = element?.Construction as oM.Physical.Constructions.Construction; + if (construction?.Layers == null) + return double.NaN; + + return construction.Layers.Sum(layer => layer.Thickness); + } + + /***************************************************/ + + [Description("Returns the centroid of the PadFoundation outer boundary as a Point.")] + [Input("element", "PadFoundation element to compute the centroid for.")] + [Output("centroid", "Centroid of the PadFoundation outer boundary point.")] + public static Point PadFoundationCentroid(this PadFoundation element) + { + return element?.PadFoundationOutline()?.Centroid(); + } + + /***************************************************/ + } +} + + diff --git a/Revit_Core_Engine/Query/Space.cs b/Revit_Core_Engine/Query/Space.cs index 02c990c45..9c0fcf882 100644 --- a/Revit_Core_Engine/Query/Space.cs +++ b/Revit_Core_Engine/Query/Space.cs @@ -36,7 +36,6 @@ public static partial class Query /**** Public methods ****/ /***************************************************/ - [PreviousVersion("9.1", "BH.Revit.Engine.Core.Query.Space(Autodesk.Revit.DB.Element, System.Collections.Generic.IEnumerable, System.Boolean)")] [Description("Returns the Revit Space that contains the given element.")] [Input("element", "The Revit element for which to find the containing Space.")] [Input("spaces", "An optional collection of Revit Spaces to search. If not provided, all Spaces in the element's document will be used.")] @@ -102,7 +101,17 @@ public static Space Space(this Element element, IEnumerable spaces, bool if (!findClosestIfNotContained) return null; - // 4. If not found, try find closest space in connector directions (for MEP elements) + // 4. If still not found, try find closest above (Z direction) + Space closestAbove = locationPoint.FindClosestSpaceInDirection(XYZ.BasisZ, spaces, maxDistance: 1); // 1 feet max distance + if (closestAbove != null) + return closestAbove; + + // 5. If still not found, try find closest below (negative Z direction) + Space closestBelow = locationPoint.FindClosestSpaceInDirection(-XYZ.BasisZ, spaces, maxDistance: 10); // 10 feet max distance + if (closestBelow != null) + return closestBelow; + + // 6. If not found, try find closest space in connector directions (for MEP elements) var connectors = element.Connectors()?.OrderByDescending(x => x.GetMEPConnectorInfo().IsPrimary).ToList(); if (connectors != null && connectors.Any()) { @@ -123,16 +132,6 @@ public static Space Space(this Element element, IEnumerable spaces, bool } } - // 5. If still not found, try find closest above (Z direction) - Space closestAbove = locationPoint.FindClosestSpaceInDirection(XYZ.BasisZ, spaces, maxDistance: 1); // 1 feet max distance - if (closestAbove != null) - return closestAbove; - - // 6. If still not found, try find closest below (negative Z direction) - Space closestBelow = locationPoint.FindClosestSpaceInDirection(-XYZ.BasisZ, spaces, maxDistance: 10); // 10 feet max distance - if (closestBelow != null) - return closestBelow; - // Not found return null; } diff --git a/Revit_Core_Engine/Query/StructuralMaterial.cs b/Revit_Core_Engine/Query/StructuralMaterial.cs index 96d769496..101486d0b 100644 --- a/Revit_Core_Engine/Query/StructuralMaterial.cs +++ b/Revit_Core_Engine/Query/StructuralMaterial.cs @@ -37,7 +37,6 @@ public static partial class Query /**** Public methods ****/ /***************************************************/ - [PreviousVersion("9.1", "BH.Revit.Engine.Core.Query.FramingMaterial(Autodesk.Revit.DB.FamilyInstance, BH.oM.Adapters.Revit.Settings.RevitSettings, System.Collections.Generic.Dictionary>)")] [Description("Extracts a BHoM representation of material from Revit FamilyInstance representing a framing element.")] [Input("familyInstance", "Revit FamilyInstance to be queried.")] [Input("settings", "Revit adapter settings to be used while performing the query.")] diff --git a/Revit_Core_Engine/Query/TransformToOriginInXY.cs b/Revit_Core_Engine/Query/TransformToOriginInXY.cs new file mode 100644 index 000000000..4cdcc071b --- /dev/null +++ b/Revit_Core_Engine/Query/TransformToOriginInXY.cs @@ -0,0 +1,73 @@ +/* + * This file is part of the Buildings and Habitats object Model (BHoM) + * Copyright (c) 2015 - 2026, the respective contributors. All rights reserved. + * + * Each contributor holds copyright over their respective contributions. + * The project versioning (Git) records all such contribution source information. + * + * + * The BHoM is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, or + * (at your option) any later version. + * + * The BHoM is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this code. If not, see . + */ + +using BH.Engine.Geometry; +using BH.oM.Base.Attributes; +using BH.oM.Geometry; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Revit.Engine.Core +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Computes translation and rotation to orient a Polyline to the origin in XY.")] + [Input("outline", "Polyline to transform.")] + [Output("result", "Centroid projected to XY and rotation angle in radians around Z.")] + public static (Vector, double) TransformToOriginInXY(this Polyline outline) + { + Vector translation = null; + double rotation = double.NaN; + + List pts = outline?.ControlPoints; + if (pts == null || pts.Count < 3) + return (translation, rotation); + + Point centroid = outline.Centroid(); + if (centroid == null) + return (translation, rotation); + + translation = (new Point() - centroid); + + // 1. Find dominant edge direction + // 2. Find longest edge parallel to the dominant direction + // 3. Rotate the longest edge to X axis and check if the start point is above or below the centroid to determine the rotation direction + Vector dominantEdge = outline.DominantEdgeDirection(Tolerance.Distance); + if (dominantEdge != null) + { + Line longestEdge = outline.SubParts().Where(x => x.Direction().IsParallel(dominantEdge, Tolerance.Angle) != 0).OrderByDescending(x => x.Length()).First(); + rotation = dominantEdge.SignedAngle(Vector.XAxis, Vector.ZAxis); + Point orientedStart = longestEdge.Start.Rotate(centroid, Vector.ZAxis, rotation); + if (orientedStart.Y > centroid.Y) + rotation -= Math.PI; + } + + return (translation, rotation); + } + } +} diff --git a/Revit_Core_Engine/Query/ViewBox.cs b/Revit_Core_Engine/Query/ViewBox.cs index 64d82d3fe..64d875cf1 100644 --- a/Revit_Core_Engine/Query/ViewBox.cs +++ b/Revit_Core_Engine/Query/ViewBox.cs @@ -50,6 +50,20 @@ public static BoundingBoxXYZ ViewBoxElevation(this IEnumerable elements /***************************************************/ + [Description("Returns the combined bounding box containing given Revit solids, aligned with the right given direction, inflated by a given offset.")] + [Input("solids", "Solids to find the elevation box for.")] + [Input("direction", "Direction, along which the elevation is meant to be made.")] + [Input("sideOffset", "Offset value to inflate the elevation view in width and height, in metres.")] + [Input("frontOffset", "Offset distance between the solid front and the origin of the view, in metres. Use negative value to create section.")] + [Input("backOffset", "Offset distance between the solid back and the back edge of the view, in metres.")] + [Output("box", "Elevation box of the input Revit solids.")] + public static BoundingBoxXYZ ViewBoxElevation(this IEnumerable solids, XYZ direction, double sideOffset, double frontOffset, double backOffset) + { + return solids.ViewBoxSection(direction, XYZ.BasisZ, sideOffset, frontOffset, backOffset); + } + + /***************************************************/ + [Description("Returns the bounding box containing a given Revit element, aligned with the given right direction, inflated by a given offset.")] [Input("element", "Element to find the elevation box for.")] [Input("direction", "Direction, along which the elevation is meant to be made.")] @@ -80,6 +94,33 @@ public static BoundingBoxXYZ ViewBoxSection(this IEnumerable elements, return null; } + Options options = new Options(); + options.DetailLevel = Autodesk.Revit.DB.ViewDetailLevel.Fine; + options.ComputeReferences = false; + options.IncludeNonVisibleObjects = false; + + IEnumerable solids = elements?.SelectMany(x => x.Solids(options)); + return solids.ViewBoxSection(rightDirection, upDirection, sideOffset, frontOffset, backOffset); + } + + /***************************************************/ + + [Description("Returns the combined bounding box containing given Revit solids, aligned with the given right and up direction, inflated by a given offset.")] + [Input("solids", "Solids to find the elevation box for.")] + [Input("rightDirection", "Direction, along which the elevation is meant to be made.")] + [Input("upDirection", "Direction pointing up in the created section. Needs to be perpendicular to upDirection.")] + [Input("sideOffset", "Offset value to inflate the elevation view in width and height, in metres.")] + [Input("frontOffset", "Offset distance between the solid front and the origin of the view, in metres. Use negative value to create section.")] + [Input("backOffset", "Offset distance between the solid back and the back edge of the view, in metres.")] + [Output("box", "Elevation box of the input Revit solids.")] + public static BoundingBoxXYZ ViewBoxSection(this IEnumerable solids, XYZ rightDirection, XYZ upDirection, double sideOffset, double frontOffset, double backOffset) + { + if (solids == null || !solids.Any()) + { + BH.Engine.Base.Compute.RecordError("Could create view box for null Revit solids."); + return null; + } + if (upDirection == null || rightDirection == null) { BH.Engine.Base.Compute.RecordError("Could create view box with null direction."); @@ -108,7 +149,7 @@ public static BoundingBoxXYZ ViewBoxSection(this IEnumerable elements, Transform transform = orientationMatrix.ToRevit().TryFixIfNonConformal(); // Bounds of the element in the provided coordinate system - BoundingBoxXYZ boundingBox = elements.PhysicalBounds(transform.Inverse); + BoundingBoxXYZ boundingBox = solids.Bounds(transform.Inverse); boundingBox.Transform = transform; // Front offset and depth diff --git a/Revit_Core_Engine/Revit_Core_Engine.csproj b/Revit_Core_Engine/Revit_Core_Engine.csproj index 27fa14b91..4514c7144 100644 --- a/Revit_Core_Engine/Revit_Core_Engine.csproj +++ b/Revit_Core_Engine/Revit_Core_Engine.csproj @@ -6,7 +6,7 @@ BHoM Copyright © https://github.com/BHoM BH.Revit.Engine.Core - 9.1.0.0 + 9.2.0.0 diff --git a/Revit_Engine/Revit_Engine.csproj b/Revit_Engine/Revit_Engine.csproj index aa65d7b53..38eb18538 100644 --- a/Revit_Engine/Revit_Engine.csproj +++ b/Revit_Engine/Revit_Engine.csproj @@ -8,7 +8,7 @@ BHoM Copyright © https://github.com/BHoM BH.Engine.Revit - 9.1.0.0 + 9.2.0.0 diff --git a/Revit_oM/Revit_oM.csproj b/Revit_oM/Revit_oM.csproj index 7bb535e3b..26344f79e 100644 --- a/Revit_oM/Revit_oM.csproj +++ b/Revit_oM/Revit_oM.csproj @@ -7,7 +7,7 @@ BHoM Copyright © https://github.com/BHoM BH.oM.Revit - 9.1.0.0 + 9.2.0.0 ..\Build\