diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index 98ed850..bd87da9 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -37,13 +37,13 @@ public static partial class Compute /**** Public methods ****/ /***************************************************/ - [Description("Perform a boolean difference operation between a main region and a list of reference regions using Clipper2.")] + [Description("Perform a boolean difference operation between a main region and a list of reference regions using Clipper2. Reference regions that do not lie on the subject plane are skipped, and the subject is kept.")] [Input("region", "The main polyline region to subtract from.")] - [Input("refRegions", "List of reference polylines to subtract from the main region.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the main region.")] + [Input("refRegions", "List of reference polylines to subtract from the main region. Regions off the subject plane are ignored.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the main region). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Tolerance for planarity checks and numerical precision. Default is Tolerance.Distance.")] [Output("result", "List of polylines representing the difference (main region minus reference regions).")] - public static List BooleanDifference(this Polyline region, List refRegions, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static List BooleanDifference(this Polyline region, List refRegions, Plane plane = null, double tolerance = Tolerance.Distance) { if (region == null || refRegions == null || region.ControlPoints.Count < 3) return null; @@ -52,24 +52,29 @@ public static List BooleanDifference(this Polyline region, List { region }; - if (curvePlane == null) + if (plane == null) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + plane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper BooleanDifference method only works for coplanar polylines."); return null; } + + // An opening on another plane must not discard the subject. Subtract only the clips that lie on this plane. + clipRegions = clipRegions.Where(x => x.ControlPoints.All(y => y != null && y.IsInPlane(plane, tolerance))).ToList(); + if (clipRegions.Count == 0) + return new List { region }; } - if (clipRegions.Any(x => x.ControlPoints.Any(y => !y.IsInPlane(curvePlane, tolerance)))) + if (!region.IsClosed(tolerance) || clipRegions.Any(x => !x.IsClosed(tolerance))) { - Base.Compute.RecordError("Clipper BooleanDifference method only works for coplanar polylines."); - return null; + Base.Compute.RecordError("Boolean Difference works on closed regions."); + return new List { region }; } // Find the orientation matrix to the global XY plane - TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); + TransformMatrix orientation = region.OrientationToGlobalXY(plane, tolerance); if (orientation == null) return null; diff --git a/Clipper_Engine/Compute/BooleanIntersection.cs b/Clipper_Engine/Compute/BooleanIntersection.cs index 3ff3d61..6e9873f 100644 --- a/Clipper_Engine/Compute/BooleanIntersection.cs +++ b/Clipper_Engine/Compute/BooleanIntersection.cs @@ -40,32 +40,38 @@ public static partial class Compute [Description("Perform a boolean intersection operation between two polylines using Clipper2.")] [Input("poly1", "The first polyline for intersection.")] [Input("poly2", "The second polyline for intersection.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the first polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the first polyline). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Tolerance for planarity checks and numerical precision. Default is Tolerance.Distance.")] [Output("result", "List of polylines representing the intersection of the two input polylines.")] - public static List BooleanIntersection(this Polyline poly1, Polyline poly2, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static List BooleanIntersection(this Polyline poly1, Polyline poly2, Plane plane = null, double tolerance = Tolerance.Distance) { if (poly1 == null || poly2 == null || poly1.ControlPoints.Count < 3 || poly2.ControlPoints.Count < 3) return null; - if (curvePlane == null) + if (!poly1.IsClosed(tolerance) || !poly2.IsClosed(tolerance)) { - curvePlane = poly1.FitPlane(); - if (poly1.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + Base.Compute.RecordError("Boolean Intersection works on closed regions."); + return new List(); + } + + if (plane == null) + { + plane = poly1.FitPlane(); + if (poly1.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper BooleanIntersection method only works for coplanar polylines."); return null; } - } - if (poly2.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) - { - Base.Compute.RecordError("Clipper BooleanIntersection method only works for coplanar polylines."); - return null; + if (poly2.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) + { + Base.Compute.RecordError("Clipper BooleanIntersection method only works for coplanar polylines."); + return null; + } } // Find the orientation matrix to the global XY plane - TransformMatrix orientation = poly1.OrientationToGlobalXY(curvePlane, tolerance); + TransformMatrix orientation = poly1.OrientationToGlobalXY(plane, tolerance); if (orientation == null) return null; diff --git a/Clipper_Engine/Compute/BooleanUnion.cs b/Clipper_Engine/Compute/BooleanUnion.cs index 574b3e9..8e849d1 100644 --- a/Clipper_Engine/Compute/BooleanUnion.cs +++ b/Clipper_Engine/Compute/BooleanUnion.cs @@ -39,10 +39,10 @@ public static partial class Compute [Description("Perform a boolean union operation between multiple polylines using Clipper2.")] [Input("polylines", "List of polylines to union together.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the first polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the first polyline). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Tolerance for planarity checks and numerical precision. Default is Tolerance.Distance.")] [Output("result", "List of polylines representing the union of all input polylines.")] - public static List BooleanUnion(this List polylines, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static List BooleanUnion(this List polylines, Plane plane = null, double tolerance = Tolerance.Distance) { if (polylines == null || polylines.Count == 0) return new List(); @@ -55,10 +55,16 @@ public static List BooleanUnion(this List polylines, Plane c if (validPolylines.Count == 1) return new List { validPolylines[0] }; - if (curvePlane == null) + if (validPolylines.Any(x => !x.IsClosed(tolerance))) { - curvePlane = validPolylines[0].FitPlane(); - if (validPolylines.Any(x => x.ControlPoints.Any(y => !y.IsInPlane(curvePlane, tolerance)))) + Base.Compute.RecordError("Boolean Union works on closed regions."); + return new List(); + } + + if (plane == null) + { + plane = validPolylines[0].FitPlane(); + if (validPolylines.Any(x => x.ControlPoints.Any(y => !y.IsInPlane(plane, tolerance)))) { Base.Compute.RecordError("Clipper BooleanUnion method only works for coplanar polylines."); return new List(); @@ -66,7 +72,7 @@ public static List BooleanUnion(this List polylines, Plane c } // Find the orientation matrix to the global XY plane - TransformMatrix orientation = validPolylines[0].OrientationToGlobalXY(curvePlane, tolerance); + TransformMatrix orientation = validPolylines[0].OrientationToGlobalXY(plane, tolerance); if (orientation == null) return new List(); diff --git a/Clipper_Engine/Compute/Offset.cs b/Clipper_Engine/Compute/Offset.cs index d611ac4..bebcb89 100644 --- a/Clipper_Engine/Compute/Offset.cs +++ b/Clipper_Engine/Compute/Offset.cs @@ -40,10 +40,10 @@ public static partial class Compute [Description("Offset a curve by the given distance (using Clipper http://www.angusj.com/delphi/clipper.php). Method only works for closed, planar polylines.")] [Input("polyline", "A BHoM Polyline representing the curve to offset.")] [Input("distance", "The distance by which to offset the curve (-Ve is inwards).")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the polyline). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Tolerance to be used for planarity and closedness checks as well as for offset computations.")] [Output("polylines", "Input polylines after offset.")] - public static List Offset(this Polyline polyline, double distance = 0, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static List Offset(this Polyline polyline, double distance = 0, Plane plane = null, double tolerance = Tolerance.Distance) { if (polyline == null) return null; @@ -57,10 +57,10 @@ public static List Offset(this Polyline polyline, double distance = 0, return null; } - if (curvePlane == null) + if (plane == null) { - curvePlane = polyline.FitPlane(); - if (polyline.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + plane = polyline.FitPlane(); + if (polyline.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper Offset method only works for planar polylines."); return null; @@ -68,7 +68,7 @@ public static List Offset(this Polyline polyline, double distance = 0, } // Find orientation matrix to the global XY plane - TransformMatrix orientation = polyline.OrientationToGlobalXY(curvePlane, tolerance); + TransformMatrix orientation = polyline.OrientationToGlobalXY(plane, tolerance); if (orientation == null) return null; diff --git a/Clipper_Engine/Query/CullDuplicates.cs b/Clipper_Engine/Query/CullDuplicates.cs new file mode 100644 index 0000000..391669a --- /dev/null +++ b/Clipper_Engine/Query/CullDuplicates.cs @@ -0,0 +1,62 @@ +/* + * 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.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.Clipper +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Removes duplicate polylines. Two polylines are duplicates when their control points are equal in the same order. Either list may be shifted, so a different starting point still counts as a duplicate. If plane is null, control points are compared directly. If plane is not null, they are projected onto the plane before comparison.")] + [Input("polylines", "The collection of polylines to cull duplicates from.")] + [Input("plane", "If null, control points are compared directly. If not null, control points are projected onto this plane before comparison.")] + [Output("polylines", "The collection of polylines with duplicates removed.")] + public static List CullDuplicates(this IEnumerable polylines, Plane plane = null) + { + if (polylines == null) + return new List(); + + List culled = new List(); + foreach (Polyline polyline in polylines) + { + if (polyline == null) + continue; + + if (!culled.Any(x => x.IsSame(polyline, plane))) + culled.Add(polyline); + } + + return culled; + } + + /***************************************************/ + } +} diff --git a/Clipper_Engine/Query/DoesOverlap.cs b/Clipper_Engine/Query/DoesOverlap.cs new file mode 100644 index 0000000..fda1ea1 --- /dev/null +++ b/Clipper_Engine/Query/DoesOverlap.cs @@ -0,0 +1,52 @@ +/* + * 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.oM.Base.Attributes; +using BH.oM.Geometry; +using System.Collections.Generic; +using System.ComponentModel; +namespace BH.Engine.Clipper +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Checks if two polylines intersect on a plane without one fully containing the other.")] + [Input("poly1", "First polyline.")] + [Input("poly2", "Second polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the first polyline). If not null, 3D geometry is processed using this plane.")] + [Input("tolerance", "Distance tolerance.")] + [Output("intersects", "True if the polylines intersect.")] + public static bool DoesOverlap(this Polyline poly1, Polyline poly2, Plane plane = null, double tolerance = Tolerance.Distance) + { + if (poly1 == null || poly2 == null || poly1.ControlPoints.Count < 3 || poly2.ControlPoints.Count < 3) + return false; + + List intersection = Compute.BooleanIntersection(poly1, poly2, plane, tolerance); + return intersection != null && intersection.Count > 0; + } + + /***************************************************/ + } +} diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Query/IsContaining.cs similarity index 53% rename from Clipper_Engine/Compute/IsContaining.cs rename to Clipper_Engine/Query/IsContaining.cs index 9107de8..1b840c3 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Query/IsContaining.cs @@ -25,6 +25,7 @@ using BH.oM.Base.Attributes; using BH.oM.Geometry; using Clipper2Lib; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; @@ -40,31 +41,33 @@ public static partial class Compute [Description("Check if a polyline contains a list of points using Clipper2 for efficient point-in-polygon testing.")] [Input("region", "The outer polyline to test against.")] [Input("points", "List of points to check if they are contained within the outer polyline.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the outer polyline.")] - [Input("acceptOnEdge", "Whether to consider points exactly on the polygon edge as contained. Default is true.")] - [Input("tolerance", "Tolerance for planarity checks and numerical precision. Default is Tolerance.Distance.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the outer polyline). If not null, 3D geometry is processed using this plane.")] + [Input("acceptOnEdge", "Whether to consider points exactly on the polygon edge as contained.")] + [Input("tolerance", "Tolerance for planarity checks and numerical precision.")] [Output("contains", "True if all points are contained within the outer polyline, false otherwise.")] - public static bool IsContaining(this Polyline region, List points, Plane curvePlane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline region, List points, Plane plane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { if (region == null || points == null || region.ControlPoints.Count < 3 || points.Count == 0) return false; - if (curvePlane == null) + if (!region.IsClosed(tolerance)) + return false; + + if (plane == null) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + plane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } - } - // Check if all points are coplanar with the outer polyline - if (points.Any(x => !x.IsInPlane(curvePlane, tolerance))) - return false; + if (points.Any(x => !x.IsInPlane(plane, tolerance))) + return false; + } // Find the orientation matrix to the global XY plane - TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); + TransformMatrix orientation = region.OrientationToGlobalXY(plane, tolerance); if (orientation == null) return false; @@ -94,41 +97,98 @@ public static bool IsContaining(this Polyline region, List points, Plane /***************************************************/ + [Description("Checks if a point lies inside a polyline on the given plane, using Clipper2 point-in-polygon.")] + [Input("outer", "Closed polyline region.")] + [Input("point", "Point to test.")] + [Input("plane", "If not null, 3D geometry is projected onto this plane for the Clipper test (required for this overload).")] + [Input("acceptOnEdge", "If true, a point on the boundary is accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if the point is inside the polyline.")] + public static bool IsContaining(this Polyline outer, Point point, Plane plane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (point == null) + return false; + + return outer.IsContaining(new List { point }, plane, acceptOnEdge, tolerance); + } + + /***************************************************/ + + [Description("Checks whether the whole line lies inside a polyline. Boundary intersections split the line, and one point between each pair is tested with the Clipper point containment method.")] + [Input("region", "Closed polyline region.")] + [Input("line", "Line to test.")] + [Input("acceptOnEdge", "If true, a sample on the boundary is accepted.")] + [Input("tolerance", "Distance tolerance used for intersections and for the Clipper point test.")] + [Output("IsContaining", "True if every sampled point of the line is inside the polyline.")] + public static bool IsContaining(this Polyline region, Line line, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (region == null || line == null) + return false; + + List parameters = new List { 0, 1 }; + foreach (Line edge in region.SubParts()) + { + foreach (Point crossPoint in edge.LineIntersections(line, false, tolerance)) + { + double x = line.ParameterAtPoint(crossPoint, tolerance); + if (x > 0 && x < 1) + parameters.Add(x); + } + } + + parameters.Sort(); + + if (!acceptOnEdge && parameters.Count > 2) + return false; + + double length = line.Length(); + List listPoints = new List { line.Start, line.End }; + for (int i = 0; i < parameters.Count - 1; i++) + { + if (Math.Abs(parameters[i + 1] - parameters[i]) * length <= tolerance) + continue; + + listPoints.Add(line.PointAtParameter((parameters[i] + parameters[i + 1]) * 0.5)); + } + + return IsContaining(region, listPoints, null, acceptOnEdge, tolerance); + } + + /***************************************************/ + [Description("Returns true if the reference polyline is fully contained within the region polyline. " + "Both polylines must be planar and coplanar. Optionally specify the plane and tolerance.")] [Input("region", "The outer polyline region to test for containment.")] [Input("refRegion", "The reference polyline to check if it is fully contained within the region.")] - [Input("curvePlane", "Optional. The plane on which both polylines lie. If null, the plane is computed from the region polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the region polyline). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Optional. The geometric tolerance for coplanarity and containment checks. Default is Tolerance.Distance.")] [Output("True if the reference polyline is fully contained within the region polyline; otherwise, false.")] - public static bool IsContaining(this Polyline region, Polyline refRegion, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline region, Polyline refRegion, Plane plane = null, double tolerance = Tolerance.Distance) { if (region == null || refRegion == null || region.ControlPoints.Count < 3 || refRegion.ControlPoints.Count < 3) return false; - if (curvePlane == null) + if (!region.IsClosed(tolerance) || !refRegion.IsClosed(tolerance)) + return false; + + if (plane == null) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + plane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } - } - if (refRegion.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) - { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); - return false; - } - - if (region.IsContaining(refRegion.ControlPoints, curvePlane, true, tolerance) == false) - { - return false; + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); + return false; + } } //If refRegion sits fully inside the outer region, the boolean difference result should be empty - return refRegion.BooleanDifference(new List { region }, curvePlane, tolerance)?.Count == 0; + return refRegion.BooleanDifference(new List { region }, plane, tolerance)?.Count == 0; } /***************************************************/ @@ -136,22 +196,43 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane [Description("Check if a polyline contains a list of reference polylines using Clipper2 for efficient polygon-in-polygon testing.")] [Input("region", "The outer polyline to test against.")] [Input("refRegions", "List of polylines to check if they are contained within the outer polyline.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the outer polyline.")] + [Input("plane", "If null, processing is in 2D (the plane is fitted from the outer polyline). If not null, 3D geometry is processed using this plane.")] [Input("tolerance", "Tolerance for planarity checks and numerical precision. Default is Tolerance.Distance.")] [Output("contains", "True if all reference polylines are fully contained within the region polyline, false otherwise.")] - public static bool IsContaining(this Polyline region, List refRegions, Plane curvePlane = null, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline region, List refRegions, Plane plane = null, double tolerance = Tolerance.Distance) { - if (curvePlane == null) + if (region == null || refRegions == null || !region.IsClosed(tolerance)) + return false; + + if (refRegions.Any(x => x == null || !x.IsClosed(tolerance))) + return false; + + if (plane == null) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + plane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } + + foreach (Polyline refRegion in refRegions) + { + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); + return false; + } + } } - return refRegions.All(x => region.IsContaining(x, curvePlane, tolerance)); + foreach (Polyline refRegion in refRegions) + { + if (!region.IsContaining(refRegion, plane, tolerance)) + return false; + } + + return true; } /***************************************************/ diff --git a/Clipper_Engine/Query/IsOnFace.cs b/Clipper_Engine/Query/IsOnFace.cs new file mode 100644 index 0000000..d84d8bd --- /dev/null +++ b/Clipper_Engine/Query/IsOnFace.cs @@ -0,0 +1,125 @@ +/* + * 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.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace BH.Engine.Clipper +{ + public static partial class Compute + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Checks if a point is inside the boundary of a planar surface but not inside one of its holes.")] + [Input("point", "A point that is potentially inside the boundary of a planar surface.")] + [Input("surface", "A planar surface that can have internal boundaries/holes.")] + [Input("tolerance", "Distance tolerance for the containment check.")] + [Output("isOnFace", "True if the point is inside the outer boundary and not inside a hole.")] + public static bool IsOnFace(this Point point, PlanarSurface surface, double tolerance = Tolerance.Distance) + { + if (point == null || surface?.ExternalBoundary == null) + return false; + + (Polyline outer, List inner) = PolylineBoundaries(surface); + if (outer == null) + { + BH.Engine.Base.Compute.RecordError("Clipper IsOnFace method only works for planar surfaces with polyline boundaries."); + return false; + } + + Plane plane = surface.FitPlane(tolerance); + if (!point.IsInPlane(plane, tolerance)) + return false; + + return point.IsOnFace(outer, inner, plane, tolerance); + + } + + /***************************************************/ + + //[Description("Checks if a point is inside the boundary of a planar surface but not inside one of its holes.")] + //[Input("point", "A point that is potentially inside the boundary of a planar surface.")] + //[Input("surface", "A planar surface that can have internal boundaries/holes.")] + //[Input("tolerance", "Distance tolerance for the containment check.")] + //[Output("isOnFace", "True if the point is inside the outer boundary and not inside a hole.")] + public static bool IsOnFace(this Point point, Polyline outer, List inner, Plane plane = null, double tolerance = Tolerance.Distance) + { + if (point == null || outer == null) + return false; + + if (!outer.IsClosed(tolerance)) + return false; + + if (inner != null && inner.Any(x => x != null && !x.IsClosed(tolerance))) + return false; + + if (plane == null) + { + plane = outer.FitPlane(tolerance); + if (outer.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance)) || inner.Any(y => y != null && y.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance)))) + { + Base.Compute.RecordError("Clipper IsOnFace method only works for planar polylines."); + return false; + } + + if (!point.IsInPlane(plane, tolerance)) + return false; + } + + return outer.IsContaining(point, plane, true, tolerance) && !inner.Any(x => x != null && x.IsContaining(point, plane, false, tolerance)); + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static (Polyline outer, List inner) PolylineBoundaries(PlanarSurface surface) + { + Polyline outer = surface.ExternalBoundary as Polyline; + if (outer == null) + return (null, null); + + List inner = new List(); + if (surface.InternalBoundaries == null) + return (outer, inner); + + foreach (ICurve boundary in surface.InternalBoundaries) + { + Polyline innerPolyline = boundary as Polyline; + if (innerPolyline == null) + return (null, null); + + inner.Add(innerPolyline); + } + + return (outer, inner); + } + + /***************************************************/ + } +}