diff --git a/BHoM_Engine/Query/Hash.cs b/BHoM_Engine/Query/Hash.cs index b876996bb..5488c86d8 100644 --- a/BHoM_Engine/Query/Hash.cs +++ b/BHoM_Engine/Query/Hash.cs @@ -234,6 +234,11 @@ private static string HashString(object obj, BaseComparisonConfig cc, int nestin if (BH.Engine.Base.Compute.TryRunExtensionMethod(obj, "HashString", parameters, out hashStringFromExtensionMethod)) return (string)hashStringFromExtensionMethod; + if (cc.UseGeometryHash && typeof(IGeometry).IsAssignableFrom(type)) + { + return GeometryHash((IGeometry)obj, cc, currentPropertyFullName); + } + // If the object is an IObject (= a BHoM class), let's look at its properties. // We only do this for IObjects (BHoM types) since we cannot guarantee full compatibility of the following procedure with any possible (non-BHoM) type. PropertyInfo[] properties = type.GetProperties(); @@ -285,24 +290,23 @@ private static string HashString(object obj, BaseComparisonConfig cc, int nestin /***************************************************/ - private static double GeometryHash(this IGeometry igeom) + private static string GeometryHash(this IGeometry igeom, BaseComparisonConfig comparisonConfig, string fullName) { if (igeom == null) - return default(double); + return null; if (m_GeomHashFunc == null) { var mis = Query.ExtensionMethods(typeof(IGeometry), "GeometryHash"); - m_GeomHashFunc = (Func)Delegate.CreateDelegate(typeof(Func), mis.First()); + if (!mis?.Any() ?? true) + throw new InvalidOperationException("Could not dynamically load the GeometryHash method."); + + m_GeomHashFunc = (Func)Delegate.CreateDelegate(typeof(Func), mis.First()); } - return m_GeomHashFunc(igeom); + return m_GeomHashFunc(igeom, comparisonConfig, fullName); } - private static Func m_GeomHashFunc = null; + private static Func m_GeomHashFunc = null; } -} - - - - +} \ No newline at end of file diff --git a/Geometry_Engine/Query/GeometryHash.cs b/Geometry_Engine/Query/GeometryHash.cs index e37389c76..91735d527 100644 --- a/Geometry_Engine/Query/GeometryHash.cs +++ b/Geometry_Engine/Query/GeometryHash.cs @@ -51,12 +51,12 @@ public static partial class Query "\nAdditionally, the resulting points are transformed based on the source geometry type, to remove or minimize collisions." + "\n(Any transformation so performed is translational only, in order to support geometrical tolerance, i.e. numerical distance, when comparing GeometryHashes downstream).")] [Input("bhomObj", "Input BHoMObject whose geometry will be queried by IGeometry() and which will be used for computing a Geometry Hash.")] - [Output("geomHash", "Number representing a unique signature of the input object's geometry.")] - public static string GeometryHash(this IBHoMObject bhomObj) + [Output("geomHash", "Value representing a unique signature of the input object's geometry.")] + public static string GeometryHash(this IBHoMObject bhomObj, BaseComparisonConfig comparisonConfig = null) { IGeometry igeom = bhomObj.IGeometry(); - return GeometryHash(igeom); + return GeometryHash(igeom, comparisonConfig); } /***************************************************/ @@ -66,13 +66,32 @@ public static string GeometryHash(this IBHoMObject bhomObj) "\nThe number of points is reduced to the minimum essential to determine uniquely any geometry." + "\nAdditionally, the resulting points are transformed based on the source geometry type, to remove or minimize collisions." + "\n(Any transformation so performed is translational only, in order to support geometrical tolerance, i.e. numerical distance, when comparing GeometryHashes downstream).")] - [Output("geomHash", "Number representing a unique signature of the input geometry.")] - public static string GeometryHash(this IGeometry igeometry) + [Output("geomHash", "Value representing a unique signature of the input geometry.")] + public static string GeometryHash(this IGeometry igeometry, BaseComparisonConfig comparisonConfig = null) + { + return GeometryHash(igeometry, comparisonConfig, null); + } + + /***************************************************/ + + [Description("Returns a signature of the input geometry, useful for diffing." + + "\nThe hash is computed as a serialised array representing the coordinate of significant points taken on the geometry." + + "\nThe number of points is reduced to the minimum essential to determine uniquely any geometry." + + "\nAdditionally, the resulting points are transformed based on the source geometry type, to remove or minimize collisions." + + "\n(Any transformation so performed is translational only, in order to support geometrical tolerance, i.e. numerical distance, when comparing GeometryHashes downstream).")] + [Input("igeometry", "Geometry you want to compute the hash for.")] + [Input("comparisonConfig", "Configurations on how the hash is computed, with options for numerical approximation, type exceptions and many others.")] + [Input("fullName", "Name of the property that holds the target object to calculate the hash for. This name will be used to seek any matching custom configuration to apply against the `comparisonConfig` input.")] + [Output("geomHash", "Value representing a unique signature of the input geometry.")] + public static string GeometryHash(this IGeometry igeometry, BaseComparisonConfig comparisonConfig, string fullName) { if (igeometry == null) return null; - double[] hashArray = IHashArray(igeometry); + double[] hashArray = IHashArray(igeometry, comparisonConfig, fullName); + if (hashArray == null) + return null; + byte[] byteArray = GetBytes(hashArray); if (m_SHA256Algorithm == null) @@ -93,6 +112,9 @@ public static string GeometryHash(this IGeometry igeometry) private static byte[] GetBytes(this double[] values) { + if (values == null) + return default; + return values.SelectMany(value => BitConverter.GetBytes(value)).ToArray(); } diff --git a/Geometry_Engine/Query/HashArray.cs b/Geometry_Engine/Query/HashArray.cs index c011d380c..ada212e44 100644 --- a/Geometry_Engine/Query/HashArray.cs +++ b/Geometry_Engine/Query/HashArray.cs @@ -45,13 +45,33 @@ public static partial class Query "\nThe number of points is reduced to the minimum essential to determine uniquely any geometry." + "\nAdditionally, the resulting points are transformed based on the source geometry type, to remove or minimize collisions." + "\n(Any transformation so performed is translational only, in order to support geometrical tolerance, i.e. numerical distance, when comparing GeometryHashes downstream).")] + [Input("igeometry", "Geometry you want to compute the hash array for.")] + [Input("comparisonConfig", "Configurations on how the hash array is computed, with options for numerical approximation, type exceptions and many others.")] [Output("hashArray", "Array of numbers representing a unique signature of the input geometry.")] - public static double[] IHashArray(this IGeometry igeometry) + public static double[] IHashArray(this IGeometry igeometry, BaseComparisonConfig comparisonConfig = null) + { + return HashArray(igeometry as dynamic, 0, comparisonConfig, null); + } + + /***************************************************/ + + [Description("Returns a signature of the input geometry, useful for distance-based comparisons and diffing." + + "\nThe hash is computed as an array representing the coordinate of significant points taken on the geometry." + + "\nThe number of points is reduced to the minimum essential to determine uniquely any geometry." + + "\nAdditionally, the resulting points are transformed based on the source geometry type, to remove or minimize collisions." + + "\n(Any transformation so performed is translational only, in order to support geometrical tolerance, i.e. numerical distance, when comparing GeometryHashes downstream).")] + [Input("igeometry", "Geometry you want to compute the hash array for.")] + [Input("comparisonConfig", "Configurations on how the hash array is computed, with options for numerical approximation, type exceptions and many others.")] + [Input("fullName", "Name of the property that holds the target object to calculate the hash array for. This name will be used to seek any matching custom configuration to apply against the `comparisonConfig` input.")] + [Output("hashArray", "Array of numbers representing a unique signature of the input geometry.")] + public static double[] IHashArray(this IGeometry igeometry, BaseComparisonConfig comparisonConfig, string fullName = null) { if (igeometry == null) return new double[] { }; - return HashArray(igeometry as dynamic, 0); + fullName = fullName ?? igeometry.GetType().FullName; + + return HashArray(igeometry as dynamic, 0, comparisonConfig, fullName); } @@ -66,19 +86,50 @@ public static double[] IHashArray(this IGeometry igeometry) [Description("The geometry hash of a Curve is obtained by first retrieving any Sub-part of the curve, if present." + "The ISubParts() methods is able to return the 'primitive' curves that a curve is composed of. " + "The GeometryHashes are then calculated for the individual parts and concatenated.")] - private static double[] HashArray(this ICurve curve, double translationFactor) + private static double[] HashArray(this ICurve curve, double translationFactor, BaseComparisonConfig comparisonConfig = null, string fullName = null) { + if (curve == null) + return default; + + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(ICurve).IsAssignableFrom(t)) ?? false) + return default; + List subParts = curve.ISubParts().ToList(); + if (!subParts.Any()) + if (curve is Polyline) + return HashArray(curve.IControlPoints(), + translationFactor, + comparisonConfig: comparisonConfig, + fullName: fullName.AppendPropertyName($"{nameof(ControlPoints)}")); + else + return HashArray(curve as dynamic, + translationFactor, + skipEndPoint: false, + comparisonConfig: comparisonConfig, + fullName: fullName); + List hashes = new List(); //Add hash ignoring endpoint for all but last curve for (int i = 0; i < subParts.Count - 1; i++) { - hashes.AddRange(HashArray(subParts[i] as dynamic, translationFactor, true)); + hashes.AddRange(HashArray(subParts[i] as dynamic, + translationFactor, + skipEndPoint: true, + comparisonConfig: comparisonConfig, + fullName: fullName.AppendPropertyName($"[{i}]")) + ); } - //Include endpoint for hasing for last curve - hashes.AddRange(HashArray(subParts.Last() as dynamic, translationFactor, false)); + + //Include endpoint for hashing for last curve + hashes.AddRange(HashArray(subParts.Last() as dynamic, + translationFactor, + skipEndPoint: false, + comparisonConfig: comparisonConfig, + fullName: fullName.AppendPropertyName($"[{subParts.Count - 1}]") + ) + ); return hashes.ToArray(); } @@ -86,15 +137,23 @@ private static double[] HashArray(this ICurve curve, double translationFactor) /***************************************************/ [Description("The GeometryHash for an Arc is calculated as the GeometryHash of the start, end and middle point of the Arc.")] - private static double[] HashArray(this Arc curve, double translationFactor, bool skipEndPoint = false) + private static double[] HashArray(this Arc curve, double translationFactor, bool skipEndPoint = false, BaseComparisonConfig comparisonConfig = null, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(ICurve).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Arc; - IEnumerable hash = curve.StartPoint().HashArray(translationFactor) - .Concat(curve.PointAtParameter(0.5).HashArray(translationFactor)); + IEnumerable hash = curve.StartPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{fullName}.{nameof(StartPoint)}")) + .Concat(curve.PointAtParameter(0.5).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(PointAtParameter)}(5e-1)")) + ); if (!skipEndPoint) - hash = hash.Concat(curve.EndPoint().HashArray(translationFactor)); + hash = hash.Concat(curve.EndPoint().HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(EndPoint)}"))); return hash.ToArray(); } @@ -102,15 +161,25 @@ private static double[] HashArray(this Arc curve, double translationFactor, bool /***************************************************/ [Description("The GeometryHash for an Circle is calculated as the GeometryHash of the start, 1/3rd and 2/3rd points of the Circle.")] - private static double[] HashArray(this Circle curve, double translationFactor) + private static double[] HashArray(this Circle curve, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Circle).IsAssignableFrom(t)) ?? false) + return default; + // The input `skipEndPoint` is not used here because Circles do not have a clearly defined endpoint to be used in a chain of segment curves. translationFactor += (int)TypeTranslationFactor.Circle; - IEnumerable hash = curve.StartPoint().HashArray(translationFactor) - .Concat(curve.PointAtParameter(0.33).HashArray(translationFactor)) - .Concat(curve.PointAtParameter(0.66).HashArray(translationFactor)); + IEnumerable hash = curve.StartPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(StartPoint)}")) + .Concat(curve.PointAtParameter(0.33).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(PointAtParameter)}(33e-2)")) + ).Concat(curve.PointAtParameter(0.66).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(PointAtParameter)}(66e-2)")) + ); return hash.ToArray(); } @@ -118,15 +187,25 @@ private static double[] HashArray(this Circle curve, double translationFactor) /***************************************************/ [Description("The GeometryHash for an Ellipse is calculated as the GeometryHash of the start, 1/3rd and 2/3rd points of the Ellipse.")] - private static double[] HashArray(this Ellipse curve, double translationFactor) + private static double[] HashArray(this Ellipse curve, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Ellipse).IsAssignableFrom(t)) ?? false) + return default; + // The input `skipEndPoint` is not used here because Ellipses do not have a clearly defined endpoint to be used in a chain of segment curves. translationFactor += (int)TypeTranslationFactor.Ellipse; - IEnumerable hash = curve.StartPoint().HashArray(translationFactor) - .Concat(curve.PointAtParameter(0.33).HashArray(translationFactor)) - .Concat(curve.PointAtParameter(0.66).HashArray(translationFactor)); + IEnumerable hash = curve.StartPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(StartPoint)}")) + .Concat(curve.PointAtParameter(0.33).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(PointAtParameter)}(33e-2)")) + ).Concat(curve.PointAtParameter(0.66).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(PointAtParameter)}(66e-2)")) + ); return hash.ToArray(); } @@ -134,16 +213,25 @@ private static double[] HashArray(this Ellipse curve, double translationFactor) /***************************************************/ [Description("The GeometryHash for a Line is calculated as the GeometryHash of the start and end point of the Line.")] - private static double[] HashArray(this Line curve, double translationFactor, bool skipEndPoint = false) + private static double[] HashArray(this Line curve, double translationFactor, BaseComparisonConfig comparisonConfig, bool skipEndPoint = false, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Line).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Line; if (skipEndPoint) - return curve.StartPoint().HashArray(translationFactor); - - return curve.StartPoint().HashArray(translationFactor) - .Concat(curve.EndPoint().HashArray(translationFactor)) - .ToArray(); + return curve.StartPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(StartPoint)}")); + + return curve.StartPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(StartPoint)}")) + .Concat(curve.EndPoint().HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(EndPoint)}")) + ).ToArray(); } /***************************************************/ @@ -151,15 +239,20 @@ private static double[] HashArray(this Line curve, double translationFactor, boo [Description("The GeometryHash for a NurbsCurve is obtained by getting moving the control points " + "by a translation factor composed by the weights and a subarray of the knot vector. " + "The subarray is made by picking as many elements from the knot vector as the curve degree value.")] - private static double[] HashArray(this NurbsCurve curve, double translationFactor) + private static double[] HashArray(this NurbsCurve curve, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(NurbsCurve).IsAssignableFrom(t)) ?? false) + return default; + // The input `skipEndPoint` is not used here because Nurbs may well extend or end before the last ControlPoint. // Also consider complex situations like Periodic curves. - int curveDegree = curve.Degree(); + int curveDegree = Math.Abs(curve.Degree()); if (curveDegree == 1) - return BH.Engine.Geometry.Create.Polyline(curve.ControlPoints).HashArray(translationFactor); + return BH.Engine.Geometry.Create.Polyline(curve.ControlPoints).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(curve.ControlPoints)}")); translationFactor += (int)TypeTranslationFactor.NurbsCurve; @@ -169,8 +262,14 @@ private static double[] HashArray(this NurbsCurve curve, double translationFacto for (int i = 0; i < controlPointsCount; i++) { - double sum = curve.Knots.GetRange(i, curveDegree).Sum(); - double[] doubles = curve.ControlPoints[i].HashArray(sum + curve.Weights[i] + translationFactor); + // Use the sum of the knots plus the i-Weight to obtain an unique traslation factor. + double knotsSum = 0; + if (i < curve.Knots.Count - 1 && curveDegree < curve.Knots.Count - 1) + knotsSum = curve.Knots.GetRange(Math.Min(i, curve.Knots.Count), Math.Min(curve.Knots.Count - 1 - i, curveDegree)).Sum(); + + double[] doubles = curve.ControlPoints[i].HashArray(knotsSum + curve.Weights.ElementAtOrDefault(i) + translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(curve.ControlPoints)}[{i}]")); concatenated.AddRange(doubles); } @@ -187,20 +286,31 @@ private static double[] HashArray(this NurbsCurve curve, double translationFacto /**** Surfaces ****/ /***************************************************/ - private static double[] HashArray(this ISurface obj, double translationFactor) + private static double[] HashArray(this ISurface obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { - return HashArray(obj as dynamic, translationFactor); + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(ISurface).IsAssignableFrom(t)) ?? false) + return default; + + return HashArray(obj as dynamic, translationFactor, comparisonConfig, fullName); } /***************************************************/ [Description("The GeometryHash for a PlanarSurface is calculated as the GeometryHash of the External and Internal boundary curves, then concatenated.")] - private static double[] HashArray(this PlanarSurface obj, double translationFactor) + private static double[] HashArray(this PlanarSurface obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(PlanarSurface).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.PlanarSurface; - return obj.ExternalBoundary.HashArray(translationFactor) - .Concat(obj.InternalBoundaries.SelectMany(ib => ib.HashArray(translationFactor))).ToArray(); + return obj.ExternalBoundary.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.ExternalBoundary)}")) + .Concat(obj.InternalBoundaries.SelectMany((ib, i) => ib.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.InternalBoundaries)}[{i}]")) + )).ToArray(); } /***************************************************/ @@ -208,22 +318,38 @@ private static double[] HashArray(this PlanarSurface obj, double translationFact [Description("The GeometryHash for an Extrusion is calculated by translating the extrusion curve with the extrusion direction vector." + "A first GeometryHash is calculated for this translated curve. " + "Then, the GeometryHash of the (non-translated) extrusion curve is concatenated to the first hash to make it more reliable.")] - private static double[] HashArray(this Extrusion obj, double translationFactor) + private static double[] HashArray(this Extrusion obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Extrusion).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Extrusion; - return obj.Curve.ITranslate(obj.Direction).HashArray(translationFactor) - .Concat(obj.Curve.HashArray(translationFactor)).ToArray(); + return obj.Curve.ITranslate(obj.Direction).HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Curve)}")) + .Concat(obj.Curve.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Curve)}")) + ).ToArray(); } /***************************************************/ [Description("The GeometryHash for a Loft is calculated as the GeometryHash of its curves.")] - private static double[] HashArray(this Loft obj, double translationFactor) + private static double[] HashArray(this Loft obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Loft).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Loft; - return obj.Curves.SelectMany(c => c.HashArray(translationFactor)).ToArray(); + return obj.Curves.SelectMany((c, i) => + c.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Curves)}[{i}]") + ) + ).ToArray(); } /***************************************************/ @@ -231,8 +357,11 @@ private static double[] HashArray(this Loft obj, double translationFactor) [Description("Moving control points by a translation factor composed by the weights " + "and a subarray of the knot vector. " + "The subarray is made by picking as many elements from the knot vector as the curve degree value.")] - private static double[] HashArray(this NurbsSurface obj, double translationFactor) + private static double[] HashArray(this NurbsSurface obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(NurbsSurface).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.NurbsSurface; List uv = obj.UVCount(); @@ -248,28 +377,47 @@ private static double[] HashArray(this NurbsSurface obj, double translationFacto { int ptIndex = i * uv[1] + j; double vSum = vKnots.GetRange(j, obj.VDegree).Sum(); - double[] doubles = obj.ControlPoints[ptIndex].HashArray(uSum + vSum + obj.Weights[ptIndex] + translationFactor); + double[] doubles = obj.ControlPoints[ptIndex].HashArray(uSum + vSum + obj.Weights[ptIndex] + translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.ControlPoints)}[{ptIndex}]")); concatenated.AddRange(doubles); } } return concatenated - .Concat(obj.InnerTrims.SelectMany(it => it.HashArray(translationFactor))) - .Concat(obj.OuterTrims.SelectMany(it => it.HashArray(translationFactor))).ToArray(); + .Concat(obj.InnerTrims.SelectMany((it, i) => + it.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.InnerTrims)}[{i}]")) + ) + ) + .Concat(obj.OuterTrims.SelectMany((it, i) => + it.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.OuterTrims)}[{i}]")) + ) + ).ToArray(); } /***************************************************/ [Description("The GeometryHash for a Pipe is calculated as the GeometryHash of its centreline translated by its radius," + "then concatenated with the GeometryHash of its centreline's StartPoint for extra reliability.")] - private static double[] HashArray(this Pipe obj, double translationFactor) + private static double[] HashArray(this Pipe obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Pipe).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Pipe; - double[] result = obj.Centreline.HashArray(translationFactor + obj.Radius); + double[] result = obj.Centreline.HashArray(translationFactor + obj.Radius, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Centreline)}")); if (obj.Capped) - result.Concat(obj.Centreline.IStartPoint().HashArray(translationFactor + obj.Radius)); + result.Concat( + obj.Centreline.IStartPoint().HashArray(translationFactor + obj.Radius, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Centreline)}.{nameof(StartPoint)}")) // StartPoint's method name must not be passed as IStartPoint here. + ); return result; } @@ -277,22 +425,34 @@ private static double[] HashArray(this Pipe obj, double translationFactor) /***************************************************/ [Description("The GeometryHash for a PolySurface is calculated as the GeometryHash of the individual surfaces.")] - private static double[] HashArray(this PolySurface obj, double translationFactor) + private static double[] HashArray(this PolySurface obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { - return obj.Surfaces.SelectMany(s => s.HashArray(translationFactor)).ToArray(); + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(PolySurface).IsAssignableFrom(t)) ?? false) + return default; + + return obj.Surfaces.SelectMany((s, i) => + s.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Surfaces)}[{i}]")) + ).ToArray(); } /***************************************************/ [Description("The GeometryHash for a SurfaceTrim is calculated as the GeometryHash of its Curve3d.")] - private static double[] HashArray(this SurfaceTrim obj, double translationFactor) + private static double[] HashArray(this SurfaceTrim obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(SurfaceTrim).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.SurfaceTrim; // We only consider the Curve3D in order to avoid being redundant with the Curve2D, // and allow distancing comparisons. - return obj.Curve3d.HashArray(translationFactor).ToArray(); + return obj.Curve3d.HashArray(translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Curve3d)}")).ToArray(); } @@ -302,24 +462,31 @@ private static double[] HashArray(this SurfaceTrim obj, double translationFactor [Description("The GeometryHash for a Mesh is obtained by getting the number of faces that are attached to each control point, " + "and use that count as a translation factor for control points.")] - private static double[] HashArray(this Mesh obj, double translationFactor) + private static double[] HashArray(this Mesh obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Mesh).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Mesh; var dic = new Dictionary(); + List result = new List(); - for (int i = 0; i < obj.Faces.Count; i++) - { - foreach (var faceIndex in obj.Faces[i].FaceIndices()) + if (!comparisonConfig?.TypeExceptions?.Any(t => typeof(Face).IsAssignableFrom(t)) ?? true) + for (int i = 0; i < obj.Faces.Count; i++) { - if (dic.ContainsKey(faceIndex)) - dic[faceIndex] += i; - else - dic[faceIndex] = i; + // If Points are excluded from the HashArray, include at least "topological" information i.e. the faces + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Point).IsAssignableFrom(t)) ?? false) + result.AddRange(obj.Faces[i].FaceIndices().Select(n => n)); + + foreach (var faceIndex in obj.Faces[i].FaceIndices()) + { + if (dic.ContainsKey(faceIndex)) + dic[faceIndex] += i; + else + dic[faceIndex] = i; + } } - } - - List result = new List(); for (int i = 0; i < obj.Vertices.Count; i++) { @@ -327,7 +494,13 @@ private static double[] HashArray(this Mesh obj, double translationFactor) if (!dic.TryGetValue(i, out pointTranslationFactor)) pointTranslationFactor = 0; - result.AddRange(obj.Vertices[i].HashArray(pointTranslationFactor + translationFactor)); + result.AddRange( + obj.Vertices[i].HashArray( + pointTranslationFactor + translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Vertices)}[{i}]") + ) ?? new double[0] + ); } return result.ToArray(); @@ -337,24 +510,31 @@ private static double[] HashArray(this Mesh obj, double translationFactor) [Description("The GeometryHash for a Mesh3D is obtained by getting the number of faces that are attached to each control point, " + "and using that count as a translation factor for control points.")] - private static double[] HashArray(this Mesh3D obj, double translationFactor) + private static double[] HashArray(this Mesh3D obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Mesh3D).IsAssignableFrom(t)) ?? false) + return default; + translationFactor += (int)TypeTranslationFactor.Mesh3D; var dic = new Dictionary(); + List result = new List(); - for (int i = 0; i < obj.Faces.Count; i++) - { - foreach (var faceIndex in obj.Faces[i].FaceIndices()) + if (!comparisonConfig?.TypeExceptions?.Any(t => typeof(Face).IsAssignableFrom(t)) ?? true) + for (int i = 0; i < obj.Faces.Count; i++) { - if (dic.ContainsKey(faceIndex)) - dic[faceIndex] += i; - else - dic[faceIndex] = i; + // If Points are excluded from the HashArray, include at least "topological" information i.e. the faces + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Point).IsAssignableFrom(t)) ?? false) + result.AddRange(obj.Faces[i].FaceIndices().Select(n => n)); + + foreach (var faceIndex in obj.Faces[i].FaceIndices()) + { + if (dic.ContainsKey(faceIndex)) + dic[faceIndex] += i; + else + dic[faceIndex] = i; + } } - } - - List result = new List(); for (int i = 0; i < obj.Vertices.Count; i++) { @@ -362,7 +542,13 @@ private static double[] HashArray(this Mesh3D obj, double translationFactor) if (!dic.TryGetValue(i, out pointTranslationFactor)) pointTranslationFactor = 0; - result.AddRange(obj.Vertices[i].HashArray(pointTranslationFactor + translationFactor)); + result.AddRange( + obj.Vertices[i].HashArray( + pointTranslationFactor + translationFactor, + comparisonConfig, + fullName.AppendPropertyName($"{nameof(obj.Vertices)}[{i}]") + ) ?? new double[0] + ); } return result.ToArray(); @@ -370,36 +556,53 @@ private static double[] HashArray(this Mesh3D obj, double translationFactor) /***************************************************/ - private static double[] HashArray(this IEnumerable points, double typeTranslationFactor) + private static double[] HashArray(this IEnumerable points, double typeTranslationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { - return points.SelectMany(p => p.HashArray(typeTranslationFactor)).ToArray(); + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(IEnumerable).IsAssignableFrom(t)) ?? false) + return default; + + return points.SelectMany((p, i) => p.HashArray(typeTranslationFactor, comparisonConfig, fullName == null ? null : $"{fullName}[{i}]")).ToArray(); } /***************************************************/ [Description("The GeometryHash for a CompositeGeometry is given as the concatenated GeometryHash of the single elements composing it.")] - private static double[] HashArray(this CompositeGeometry obj, double translationFactor) + private static double[] HashArray(this CompositeGeometry obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { - return obj.Elements.SelectMany(c => c.IHashArray()).ToArray(); + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(CompositeGeometry).IsAssignableFrom(t)) ?? false) + return default; + + return obj.Elements.SelectMany((c, i) => c.IHashArray(comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Elements)}[{i}]"))).ToArray(); } /***************************************************/ [Description("The GeometryHash for a Point is simply an array of 3 numbers composed by the Point X, Y and Z coordinates.")] - private static double[] HashArray(this Point p, double translationFactor) + private static double[] HashArray(this Point p, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Point).IsAssignableFrom(t)) ?? false) + return default; + + if (comparisonConfig == null) + return new double[] + { + p.X + translationFactor, + p.Y + translationFactor, + p.Z + translationFactor + }; + return new double[] { - p.X + translationFactor, - p.Y + translationFactor, - p.Z + translationFactor + (p.X + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(p.X)}"), comparisonConfig), + (p.Y + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(p.Y)}"), comparisonConfig), + (p.Z + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(p.Z)}"), comparisonConfig) }; } /***************************************************/ // Fallback - private static double[] HashArray(this object obj, double translationFactor) + private static double[] HashArray(this object obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { throw new NotImplementedException($"Could not find a {nameof(HashArray)} method for type {obj.GetType().FullName}."); } @@ -408,20 +611,132 @@ private static double[] HashArray(this object obj, double translationFactor) /**** Other methods for "conceptual" geometry ****/ /***************************************************/ - [Description("The GeometryHash for a CompositeGeometry is given as the concatenated GeometryHash of the single elements composing it.")] - private static double[] HashArray(this Vector obj, double translationFactor) + [Description("The GeometryHash for a Vector is given as the concatenated GeometryHash of the single elements composing it.")] + private static double[] HashArray(this Vector obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Vector).IsAssignableFrom(t)) ?? false) + return default; + if (translationFactor == (double)TypeTranslationFactor.Point) translationFactor = (double)TypeTranslationFactor.Vector; + if (comparisonConfig == null) + return new double[] + { + obj.X + translationFactor, + obj.Y + translationFactor, + obj.Z + translationFactor + }; + return new double[] { - obj.X + translationFactor, - obj.Y + translationFactor, - obj.Z + translationFactor + (obj.X + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(obj.X)}"), comparisonConfig), + (obj.Y + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(obj.Y)}"), comparisonConfig), + (obj.Z + translationFactor).ValueToInclude(fullName.AppendPropertyName($"{nameof(obj.Z)}"), comparisonConfig) }; } + /***************************************************/ + + [Description("The GeometryHash for a Basis is given as the concatenated GeometryHash of the single elements composing it.")] + private static double[] HashArray(this Basis obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) + { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Basis).IsAssignableFrom(t)) ?? false) + return default; + + translationFactor = (double)TypeTranslationFactor.Basis; + + var x = obj.X.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.X)}")); + var y = obj.Y.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Y)}")); + var z = obj.Z.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Z)}")); + return x.Concat(y).Concat(z).ToArray(); + } + + /***************************************************/ + + [Description("The GeometryHash for a Basis is given as the concatenated GeometryHash of the single elements composing it.")] + private static double[] HashArray(this Cartesian obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) + { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(Cartesian).IsAssignableFrom(t)) ?? false) + return default; + + translationFactor = (double)TypeTranslationFactor.Cartesian; + + var x = obj.X.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.X)}")); + var y = obj.Y.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Y)}")); + var z = obj.Z.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Z)}")); + var o = obj.Origin.HashArray(translationFactor, comparisonConfig, fullName.AppendPropertyName($"{nameof(obj.Origin)}")); + + return x.Concat(y).Concat(z).Concat(o).ToArray(); + } + + /***************************************************/ + + [Description("The GeometryHash for a TransformMatrix is given as the concatenated numbers of the matrix.")] + private static double[] HashArray(this TransformMatrix obj, double translationFactor, BaseComparisonConfig comparisonConfig, string fullName = null) + { + if (comparisonConfig?.TypeExceptions?.Any(t => typeof(TransformMatrix).IsAssignableFrom(t)) ?? false) + return default; + + if (comparisonConfig == null) + return obj.Matrix.Cast().ToArray(); + + return obj.Matrix.Cast() + .Select(n => n.ValueToInclude(fullName.AppendPropertyName($"{nameof(obj.Matrix)}"), comparisonConfig)) + .ToArray(); + } + + /***************************************************/ + /**** Other private methods ****/ + /***************************************************/ + + [Description("The GeometryHash for a TransformMatrix is given as the concatenated numbers of the matrix.")] + private static string AppendPropertyName(this string fullName, string toAppend) + { + if (fullName == null) + return fullName; + + return $"{fullName}.{toAppend}"; + } + + /***************************************************/ + + [Description("Determine whether a certain object property should be included in the Hash computation. " + + "This is based on the property full name and the settings in the ComparisonConfig.")] + private static bool IsPropertyIncluded(string propFullName, BaseComparisonConfig cc) + { + if (cc == null || string.IsNullOrWhiteSpace(propFullName)) + return true; + + // Skip if the property is among the PropertyExceptions. + if ((cc.PropertyExceptions?.Any(pe => propFullName.EndsWith(pe) || propFullName.WildcardMatch(pe)) ?? false)) + return false; + + // If the PropertiesToConsider contains at least a value, ensure that this property is "compatible" with at least one of them. + // Compatible means to check not only that the current propFullName is among the propertiesToInclude; + // we need to consider this propFullName ALSO IF there is at least one PropertyToInclude that specifies a property that is a child of the current propFullName. + if ((cc.PropertiesToConsider?.Any() ?? false) && + !cc.PropertiesToConsider.Any(ptc => ptc.StartsWith(propFullName) || propFullName.StartsWith(ptc))) // we want to make sure that we do not exclude sub-properties to include, hence the OR condition. + return false; + + return true; + } + + /***************************************************/ + + [Description("Determine whether a certain object property should be included in the Hash computation. " + + "This is based on the property full name and the settings in the ComparisonConfig.")] + private static double ValueToInclude(this double number, string propFullName, BaseComparisonConfig cc) + { + if (cc == null || string.IsNullOrWhiteSpace(propFullName)) + return number; + + if (!IsPropertyIncluded(propFullName, cc)) + return 0; + + return BH.Engine.Base.Query.NumericalApproximation(number, propFullName, cc); + } + /***************************************************/ /**** Private fields ****/ /***************************************************/ @@ -434,7 +749,9 @@ private static double[] HashArray(this Vector obj, double translationFactor) "like e.g. a 3-point Polyline and an Arc that passes through the same points.")] private enum TypeTranslationFactor { - Vector = -1, + Cartesian = -3 * m_ToleranceMultiplier, + Basis = -2 * m_ToleranceMultiplier, + Vector = -1 * m_ToleranceMultiplier, Point = 0, Plane = 1 * m_ToleranceMultiplier, Line = 2 * m_ToleranceMultiplier,