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..263acfc53 100644 --- a/Revit_Core_Engine/Compute/GenerateProfile.cs +++ b/Revit_Core_Engine/Compute/GenerateProfile.cs @@ -116,7 +116,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 +129,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) { @@ -174,7 +174,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) { @@ -273,7 +273,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) { @@ -811,4 +811,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/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_FreeForm.rfa b/Revit_Core_Engine/Files/Families/StructuralFoundations_FreeForm.rfa new file mode 100644 index 000000000..425275255 Binary files /dev/null and b/Revit_Core_Engine/Files/Families/StructuralFoundations_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..a91d75279 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.")] @@ -637,7 +736,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/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/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/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); + } + } +}