diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/AxisAlignedBoundingBox.cs b/src/IPA.Bcfier.Navisworks/OpenProject/AxisAlignedBoundingBox.cs new file mode 100644 index 0000000..b9dc3fa --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/AxisAlignedBoundingBox.cs @@ -0,0 +1,67 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + /// + /// Immutable implementation of an axis aligned bounding box with decimal precision. + /// + public sealed class AxisAlignedBoundingBox : IEquatable + { + /// + /// A bounding box with max values, representing an infinite bounding box + /// + public static AxisAlignedBoundingBox Infinite => + new AxisAlignedBoundingBox(Vector3.InfiniteMin, Vector3.InfiniteMax); + + /// + /// This min corner of the axis aligned bounding box. + /// + public Vector3 Min { get; } + + /// + /// This min corner of the axis aligned bounding box. + /// + public Vector3 Max { get; } + + public AxisAlignedBoundingBox(Vector3 min, Vector3 max) + { + Min = min; + Max = max; + } + + /// + /// Merges this bounding box with the given one and returns the resulting bounding box, that + /// is included in both bounding boxes. If there is no intersection between both bounding + /// boxes, an infinite bounding box is returned. + /// + /// The other bounding box + /// The merged bounding box. + public AxisAlignedBoundingBox MergeReduce(AxisAlignedBoundingBox box) + { + var min = new Vector3( + Math.Max(Min.X, box.Min.X), + Math.Max(Min.Y, box.Min.Y), + Math.Max(Min.Z, box.Min.Z)); + var max = new Vector3( + Math.Min(Max.X, box.Max.X), + Math.Min(Max.Y, box.Max.Y), + Math.Min(Max.Z, box.Max.Z)); + + // validity check + if (min.X < max.X && min.Y < max.Y && min.Z < max.Z) + return new AxisAlignedBoundingBox(min, max); + + return Infinite; + } + + /// + public bool Equals(AxisAlignedBoundingBox other) + { + if (other == null) + return false; + + return Min.Equals(other.Min) && Max.Equals(other.Max); + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/BcfExtensions.cs b/src/IPA.Bcfier.Navisworks/OpenProject/BcfExtensions.cs new file mode 100644 index 0000000..cafea66 --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/BcfExtensions.cs @@ -0,0 +1,131 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +using IPA.Bcfier.Models.Bcf; + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + public static class BcfExtensions + { + public static Vector3 ToVector3(this BcfViewpointVector direction) => + new Vector3( + direction.X.ToDecimal(), + direction.Y.ToDecimal(), + direction.Z.ToDecimal()); + + public static Vector3 ToVector3(this BcfViewpointPoint point) => + new Vector3( + point.X.ToDecimal(), + point.Y.ToDecimal(), + point.Z.ToDecimal()); + + /// + /// Converts a axis aligned bounding box into a list of bcf api clipping planes. + /// + /// + /// The bounding box that defines the clipping. Can contain infinite values, which are + /// interpreted as if the view is not clipped in that direction. + /// + /// + /// An optional clipping center. Important for positioning the clipping planes not too far + /// away from the model. If no clipping center is given, the center of the clipping box is + /// used, which can result in very odd clipping plane locations, if the clipping box + /// contains infinite values. + /// + /// A list of clipping planes. + public static List ToClippingPlanes( + this AxisAlignedBoundingBox clippingBox, + Vector3? clippingCenter = null) + { + Vector3 center = clippingCenter ?? (clippingBox.Min + clippingBox.Max) * 0.5m; + + var planes = new List(); + + if (clippingBox.Min.X.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(clippingBox.Min.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = -1, Y = 0, Z = 0 } + }); + } + + if (clippingBox.Min.Y.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(clippingBox.Min.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = -1, Z = 0 } + }); + } + + if (clippingBox.Min.Z.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(clippingBox.Min.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 0, Z = -1 } + }); + } + + if (clippingBox.Max.X.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(clippingBox.Max.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 1, Y = 0, Z = 0 } + }); + } + + if (clippingBox.Max.Y.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(clippingBox.Max.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 1, Z = 0 } + }); + } + + if (clippingBox.Max.Z.IsFinite()) + { + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(clippingBox.Max.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 0, Z = 1 } + }); + } + + return planes; + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/ClippingPlaneUtils.cs b/src/IPA.Bcfier.Navisworks/OpenProject/ClippingPlaneUtils.cs new file mode 100644 index 0000000..a03d52f --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/ClippingPlaneUtils.cs @@ -0,0 +1,79 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +using IPA.Bcfier.Models.Bcf; + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + public static class ClippingPlaneUtils + { + private static Vector3 XPositive() => new Vector3(1, 0, 0); + + private static Vector3 XNegative() => new Vector3(-1, 0, 0); + + private static Vector3 YPositive() => new Vector3(0, 1, 0); + + private static Vector3 YNegative() => new Vector3(0, -1, 0); + + private static Vector3 ZPositive() => new Vector3(0, 0, 1); + + private static Vector3 ZNegative() => new Vector3(0, 0, -1); + + /// + /// Converts a bcf clipping plane into an axis aligned clipping bounding box. The clipping + /// plane direction vector (plane normal) is compared to the coordinate axes. If the angle + /// is below a given delta, the plane is converted to one side of an axis aligned clipping + /// bounding box. The rest of the box's sides are set to max value. + /// + /// The bcf clipping plane. + /// + /// The maximum angle the plane direction can differ from a XYZ coordinate axis. + /// + /// The axis aligned bounding box. + public static AxisAlignedBoundingBox ToAxisAlignedBoundingBox(this BcfViewpointClippingPlane plane, decimal delta = 0) + { + var infiniteMax = new Vector3(decimal.MaxValue, decimal.MaxValue, decimal.MaxValue); + var infiniteMin = new Vector3(decimal.MinValue, decimal.MinValue, decimal.MinValue); + + var normal = new Vector3( + (decimal)plane.Direction.X, + (decimal)plane.Direction.Y, + (decimal)plane.Direction.Z); + + var boundingBox = new AxisAlignedBoundingBox(infiniteMin, infiniteMax); + + if (normal.AngleBetween(ZPositive()) < delta) + { + var max = new Vector3(decimal.MaxValue, decimal.MaxValue, (decimal)plane.Location.Z); + boundingBox = new AxisAlignedBoundingBox(infiniteMin, max); + } + else if (normal.AngleBetween(ZNegative()) < delta) + { + var min = new Vector3(decimal.MinValue, decimal.MinValue, (decimal)plane.Location.Z); + boundingBox = new AxisAlignedBoundingBox(min, infiniteMax); + } + else if (normal.AngleBetween(YPositive()) < delta) + { + var max = new Vector3(decimal.MaxValue, (decimal)plane.Location.Y, decimal.MaxValue); + boundingBox = new AxisAlignedBoundingBox(infiniteMin, max); + } + else if (normal.AngleBetween(YNegative()) < delta) + { + var min = new Vector3(decimal.MinValue, (decimal)plane.Location.Y, decimal.MinValue); + boundingBox = new AxisAlignedBoundingBox(min, infiniteMax); + } + else if (normal.AngleBetween(XPositive()) < delta) + { + var max = new Vector3((decimal)plane.Location.X, decimal.MaxValue, decimal.MaxValue); + boundingBox = new AxisAlignedBoundingBox(infiniteMin, max); + } + else if (normal.AngleBetween(XNegative()) < delta) + { + var min = new Vector3((decimal)plane.Location.X, decimal.MinValue, decimal.MinValue); + boundingBox = new AxisAlignedBoundingBox(min, infiniteMax); + } + + return boundingBox; + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/Extensions.cs b/src/IPA.Bcfier.Navisworks/OpenProject/Extensions.cs new file mode 100644 index 0000000..ef27481 --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/Extensions.cs @@ -0,0 +1,45 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + public static class Extensions + { + /// + /// Checks a decimal number against infinity. + /// + /// A decimal value + /// true, if a decimal value is finite, false otherwise + public static bool IsFinite(this decimal number) => number > decimal.MinValue && number < decimal.MaxValue; + + /// + /// Converts a double value to a decimal value without throwing a . If the double is bigger then the maximal value of decimals, + /// the decimal maximal value is returned. + /// + /// The double value + /// The converted decimal value + public static decimal ToDecimal(this double @double) + { + if (@double < 0) + return @double < (double)decimal.MinValue ? decimal.MinValue : (decimal)@double; + + return @double > (double)decimal.MaxValue ? decimal.MaxValue : (decimal)@double; + } + + /// + /// Converts a float value to a decimal value without throwing a . If the float is bigger then the maximal value of decimals, + /// the decimal maximal value is returned. + /// + /// The float value + /// The converted decimal value + public static decimal ToDecimal(this float @float) + { + if (@float < 0) + return @float < (float)decimal.MinValue ? decimal.MinValue : (decimal)@float; + + return @float > (float)decimal.MaxValue ? decimal.MaxValue : (decimal)@float; + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/Position.cs b/src/IPA.Bcfier.Navisworks/OpenProject/Position.cs new file mode 100644 index 0000000..27ccf6b --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/Position.cs @@ -0,0 +1,51 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + /// + /// This immutable class describes a directed location in 3d space. It contains a center point + /// and a forward and up vector to describe a defined orientation in 3d space. + /// + public sealed class Position : IEquatable + { + /// + /// The center position vector. + /// + public Vector3 Center { get; } + + /// + /// The forward direction vector. + /// + public Vector3 Forward { get; } + + /// + /// The up direction vector. + /// + public Vector3 Up { get; } + + public Position() + { + Center = Vector3.Zero; + Forward = Vector3.Zero; + Up = Vector3.Zero; + } + + public Position(Vector3 center, Vector3 forward, Vector3 up) + { + Center = center; + Forward = forward; + Up = up; + } + + /// + public bool Equals(Position other) + { + if (other == null) return false; + + return Center.Equals(other.Center) && + Forward.Equals(other.Forward) && + Up.Equals(other.Up); + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/OpenProject/Vector3.cs b/src/IPA.Bcfier.Navisworks/OpenProject/Vector3.cs new file mode 100644 index 0000000..882e81b --- /dev/null +++ b/src/IPA.Bcfier.Navisworks/OpenProject/Vector3.cs @@ -0,0 +1,92 @@ +// This was mostly taken from: +// https://github.com/opf/openproject-revit-add-in + +namespace IPA.Bcfier.Navisworks.OpenProject +{ + /// + /// A light-weight vector class for 3d vectors, containing values in decimal precision. + /// + public sealed class Vector3 : IEquatable + { + /// + /// A vector of max values, representing an positive infinite point. + /// + public static Vector3 InfiniteMax => + new Vector3(decimal.MaxValue, decimal.MaxValue, decimal.MaxValue); + + /// + /// A vector of min values, representing an negative infinite point. + /// + public static Vector3 InfiniteMin => + new Vector3(decimal.MinValue, decimal.MinValue, decimal.MinValue); + + /// + /// A vector of zero values, representing the zero vector. + /// + public static Vector3 Zero => new Vector3(0, 0, 0); + + public decimal X { get; } + public decimal Y { get; } + public decimal Z { get; } + + public Vector3(decimal x, decimal y, decimal z) + { + X = x; + Y = y; + Z = z; + } + + /// + /// Calculates the angle between this vector and the given one. + /// + /// The other vector. + /// The angle in radians. + public decimal AngleBetween(Vector3 vec) + { + if (Equals(Zero) || vec.Equals(Zero)) + throw new ArgumentException("no angle calculation possible for zero vectors"); + + return DecimalMath.DecimalEx.ACos(this * vec / (Euclidean() * vec.Euclidean())); + } + + /// + /// Calculates the sum of this vector and the given one. + /// + /// The left vector. + /// The right vector. + /// The dot product value. + public static Vector3 operator +(Vector3 v1, Vector3 v2) => new Vector3(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z); + + /// + /// Calculates the dot product of this vector and the given one. + /// + /// The left vector. + /// The right vector. + /// The dot product value. + public static decimal operator *(Vector3 v1, Vector3 v2) => + v1.X * v2.X + v1.Y * v2.Y + v1.Z * v2.Z; + + /// + /// Calculates the scalar product of this vector and a scalar value. + /// + /// The vector. + /// The scalar value. + /// The dot product value. + public static Vector3 operator *(Vector3 v1, decimal scalar) => + new Vector3(v1.X * scalar, v1.Y * scalar, v1.Z * scalar); + + /// + /// Calculates the euclidean distance of this vector. + /// + /// The euclidean distance. + public decimal Euclidean() => DecimalMath.DecimalEx.Sqrt(X * X + Y * Y + Z * Z); + + /// + public bool Equals(Vector3 other) + { + if (other == null) return false; + + return X == other.X && Y == other.Y && Z == other.Z; + } + } +} diff --git a/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs b/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs index 4371901..62c6a45 100644 --- a/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs +++ b/src/IPA.Bcfier.Navisworks/Services/NavisworksViewpointCreationService.cs @@ -2,6 +2,7 @@ using Autodesk.Navisworks.Api.Clash; using IPA.Bcfier.Models.Bcf; using IPA.Bcfier.Models.Clashes; +using IPA.Bcfier.Navisworks.OpenProject; using IPA.Bcfier.Navisworks.Utilities; namespace IPA.Bcfier.Navisworks.Services @@ -101,7 +102,12 @@ private BcfViewpoint GetViewpointFromNavisworksViewpoint(Viewpoint viewpoint) }; } - var selectedIfcGuids = _doc.CurrentSelection.SelectedItems.Select(selectedItem => selectedItem.InstanceGuid.ToIfcGuid()).ToList(); + var elementBoundingBoxes = new List(); + var selectedIfcGuids = _doc.CurrentSelection.SelectedItems.Select(selectedItem => + { + elementBoundingBoxes.Add(selectedItem.BoundingBox()); + return selectedItem.InstanceGuid.ToIfcGuid(); + }).ToList(); if (selectedIfcGuids.Any()) { v.ViewpointComponents = new BcfViewpointComponents @@ -114,6 +120,25 @@ private BcfViewpoint GetViewpointFromNavisworksViewpoint(Viewpoint viewpoint) }; } + if (elementBoundingBoxes.Any()) + { + // We need to construct a common bounding box for all selected elements + var minX = elementBoundingBoxes.Min(b => b.Min.X); + var minY = elementBoundingBoxes.Min(b => b.Min.Y); + var minZ = elementBoundingBoxes.Min(b => b.Min.Z); + var maxX = elementBoundingBoxes.Max(b => b.Max.X); + var maxY = elementBoundingBoxes.Max(b => b.Max.Y); + var maxZ = elementBoundingBoxes.Max(b => b.Max.Z); + + var commonBoundingBox = new BoundingBox3D(new Point3D(maxZ.FromInternal(), maxY.FromInternal(), minZ.FromInternal()), + new Point3D(minX.FromInternal(), minY.FromInternal(), maxZ.FromInternal())); + + var clippingPlanes = TransformBoundingBoxToClippingPlanes(commonBoundingBox); + + v.ClippingPlanes ??= new List(); + v.ClippingPlanes.AddRange(clippingPlanes); + } + #if NAVISWORKS_2023 || NAVISWORKS_2022 || NAVISWORKS_2021 var navisworksSnapshot = _doc.GenerateImage(ImageGenerationStyle.Scene, 1920, 1080); @@ -370,5 +395,81 @@ private static Rotation3D MultiplyRotation3D(Rotation3D r2, Rotation3D r1) rot.Normalize(); return rot; } + + private List TransformBoundingBoxToClippingPlanes(BoundingBox3D clippingBox) + { + Vector3 center = new Vector3(clippingBox.Center.X.ToDecimal(), clippingBox.Center.Y.ToDecimal(), clippingBox.Center.Z.ToDecimal()); + + var planes = new List(); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(clippingBox.Min.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = -1, Y = 0, Z = 0 } + }); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(clippingBox.Min.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = -1, Z = 0 } + }); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(clippingBox.Min.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 0, Z = -1 } + }); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(clippingBox.Max.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 1, Y = 0, Z = 0 } + }); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(clippingBox.Max.Y), + Z = Convert.ToSingle(center.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 1, Z = 0 } + }); + + planes.Add(new BcfViewpointClippingPlane + { + Location = new BcfViewpointPoint + { + X = Convert.ToSingle(center.X), + Y = Convert.ToSingle(center.Y), + Z = Convert.ToSingle(clippingBox.Max.Z) + }, + Direction = new BcfViewpointVector { X = 0, Y = 0, Z = 1 } + }); + + return planes; + } } } +