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_Engine/Modify/SetLocation.cs b/Revit_Core_Engine/Modify/SetLocation.cs index 1fa10dc84..7d7af6754 100644 --- a/Revit_Core_Engine/Modify/SetLocation.cs +++ b/Revit_Core_Engine/Modify/SetLocation.cs @@ -445,6 +445,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 ****/ @@ -493,6 +505,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 = ps.IBounds().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; @@ -561,6 +794,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()}"); } } @@ -591,6 +825,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); @@ -604,9 +839,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; 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/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); + } + + /***************************************************/ + } +}