From 2de8211b083e5dcfd2cf85cbf487dd9129087c2e Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Fri, 16 Oct 2026 13:07:28 +0200 Subject: [PATCH 01/14] Initial clean-up --- Clipper_Engine/Compute/BooleanDifference.cs | 27 +++ Clipper_Engine/Compute/IsContaining.cs | 193 ++++++++++++-------- Clipper_Engine/Compute/IsOnFace.cs | 92 ++++++++++ Clipper_Engine/Compute/Offset.cs | 36 ++++ Clipper_Engine/Query/PolygonRelations.cs | 144 +++++++++++++++ Clipper_Engine/Query/UniquePolylines.cs | 70 +++++++ 6 files changed, 482 insertions(+), 80 deletions(-) create mode 100644 Clipper_Engine/Compute/IsOnFace.cs create mode 100644 Clipper_Engine/Query/PolygonRelations.cs create mode 100644 Clipper_Engine/Query/UniquePolylines.cs diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index 98ed850..4f0fc41 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -101,6 +101,33 @@ public static List BooleanDifference(this Polyline region, List BooleanDifference(this List subjects, List clips, Plane curvePlane = null, double tolerance = Tolerance.Distance) + { + if (subjects == null || !subjects.Any()) + return subjects ?? new List(); + + if (clips == null || !clips.Any()) + return subjects; + + List result = new List(); + foreach (Polyline subject in subjects) + { + List difference = subject.BooleanDifference(clips, curvePlane, tolerance); + if (difference != null) + result.AddRange(difference); + } + + return result; + } + + /***************************************************/ } } diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 9107de8..f96b4d7 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -37,124 +37,157 @@ public static partial class Compute /**** Public methods ****/ /***************************************************/ - [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.")] - [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) + [Description("Checks if all input points lie inside a polyline on the given plane, using Clipper2 point-in-polygon.")] + [Input("outer", "Closed polyline region.")] + [Input("points", "Points to test.")] + [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [Input("acceptOnEdge", "If true, points on the boundary are accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if every point is inside the polyline.")] + public static bool IsContaining(this Polyline outer, List points, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { - if (region == null || points == null || region.ControlPoints.Count < 3 || points.Count == 0) + if (outer == null || points == null || curvePlane == null || outer.ControlPoints.Count < 3) return false; - if (curvePlane == null) - { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) - { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); - return false; - } - } + if (points.Count == 0) + return true; - // Check if all points are coplanar with the outer polyline - if (points.Any(x => !x.IsInPlane(curvePlane, tolerance))) - return false; + return AllPointsInPolygon(outer, points, curvePlane, acceptOnEdge, tolerance); + } - // Find the orientation matrix to the global XY plane - TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); - if (orientation == null) + /***************************************************/ + + [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("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [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 curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (point == null) return false; - // Transform outer polyline to the global XY plane - Polyline regionOnXY = region.OpenPolylineOnXY(orientation); + return AllPointsInPolygon(outer, new List { point }, curvePlane, acceptOnEdge, tolerance); + } - // Scale is set to run the containment test at a much larger scale for precision - double scale = 1 / tolerance; - Path64 regionPath = regionOnXY.ToClipPath(scale); + /***************************************************/ - // Transform and test each point - foreach (Point pnt in points) - { - Point64 checkPoint = pnt.Transform(orientation).ToPoint64(scale); - PointInPolygonResult checkResult = Clipper2Lib.Clipper.PointInPolygon(checkPoint, regionPath); + [Description("Checks if a polyline is contained in another polyline on the given plane, using Clipper2 point-in-polygon on the inner control points.")] + [Input("outer", "Closed polyline region.")] + [Input("inner", "Polyline to test.")] + [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [Input("acceptOnEdge", "If true, inner vertices on the outer boundary are accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if every control point of the inner polyline is inside the outer polyline.")] + public static bool IsContaining(this Polyline outer, Polyline inner, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (outer == null || inner == null || curvePlane == null) + return false; - if (checkResult == PointInPolygonResult.IsOutside) - return false; + return AllPointsInPolygon(outer, inner.ControlPoints, curvePlane, acceptOnEdge, tolerance); + } - // Check if point is on edge, but we don't accept edge hits - if (!acceptOnEdge && checkResult == PointInPolygonResult.IsOn) - return false; - } + /***************************************************/ - return true; + [Description("Checks if a line is contained in a polyline on the given plane, using Clipper2 point-in-polygon on the line start, end, and midpoint.")] + [Input("outer", "Closed polyline region.")] + [Input("line", "Line to test.")] + [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [Input("acceptOnEdge", "If true, points on the boundary are accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if the line start, end, and midpoint are inside the polyline.")] + public static bool IsContaining(this Polyline outer, Line line, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (outer == null || line == null || curvePlane == null) + return false; + + Point mid = (line.Start + line.End) * 0.5; + return AllPointsInPolygon(outer, new List { line.Start, line.End, mid }, curvePlane, 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("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) + [Description("Checks if every input point is contained by at least one polyline on the given plane.")] + [Input("regions", "Closed polyline regions.")] + [Input("points", "Points to test.")] + [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [Input("acceptOnEdge", "If true, points on a boundary are accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if every point lies inside at least one region.")] + public static bool IsContaining(this List regions, List points, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { - if (region == null || refRegion == null || region.ControlPoints.Count < 3 || refRegion.ControlPoints.Count < 3) + if (regions == null || points == null || curvePlane == null) return false; - if (curvePlane == null) + List remainingPoints = new List(points); + foreach (Polyline pLine in regions) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + if (!remainingPoints.Any()) + break; + + for (int i = remainingPoints.Count - 1; i >= 0; i--) { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); - return false; + if (pLine.IsContaining(remainingPoints[i], curvePlane, acceptOnEdge, tolerance)) + remainingPoints.RemoveAt(i); } } - if (refRegion.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) - { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); - return false; - } + return remainingPoints.Count == 0; + } - if (region.IsContaining(refRegion.ControlPoints, curvePlane, true, tolerance) == false) - { + /***************************************************/ + + [Description("Checks if the input point is contained by at least one polyline on the given plane.")] + [Input("regions", "Closed polyline regions.")] + [Input("point", "Point to test.")] + [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] + [Input("acceptOnEdge", "If true, a point on a boundary is accepted.")] + [Input("tolerance", "Distance tolerance used for orientation and scaling.")] + [Output("isContaining", "True if the point lies inside at least one region.")] + public static bool IsContaining(this List regions, Point point, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + { + if (regions == null || point == null || curvePlane == null) 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 regions.Any(x => x.IsContaining(point, curvePlane, acceptOnEdge, tolerance)); } + /***************************************************/ + /**** Private methods ****/ /***************************************************/ - [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("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) + private static bool AllPointsInPolygon(Polyline outer, IEnumerable points, Plane curvePlane, bool acceptOnEdge, double tolerance) { - if (curvePlane == null) + TransformMatrix orientation = outer.OrientationToGlobalXY(curvePlane, tolerance); + if (orientation == null) + return false; + + Polyline outerOnXY = outer.OpenPolylineOnXY(orientation, tolerance); + if (outerOnXY.ControlPoints.Count < 3) + return false; + + double scale = 1 / tolerance; + Path64 path = outerOnXY.ToClipPath(scale); + + foreach (Point point in points) { - curvePlane = region.FitPlane(); - if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) - { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); + if (point == null) + return false; + + Point ptOnXY = point.Project(curvePlane).Transform(orientation); + PointInPolygonResult result = Clipper2Lib.Clipper.PointInPolygon(ptOnXY.ToPoint64(scale), path); + if (result == PointInPolygonResult.IsOutside) + return false; + + if (result == PointInPolygonResult.IsOn && !acceptOnEdge) return false; - } } - return refRegions.All(x => region.IsContaining(x, curvePlane, tolerance)); + return true; } /***************************************************/ } } - diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs new file mode 100644 index 0000000..b741255 --- /dev/null +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -0,0 +1,92 @@ +/* + * 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; + + Plane plane = surface.FitPlane(tolerance); + if (!ContainsOnBoundary(surface.ExternalBoundary, point, plane, true, tolerance)) + return false; + + if (surface.InternalBoundaries == null) + return true; + + return !surface.InternalBoundaries.Any(x => x != null && ContainsOnBoundary(x, point, plane, false, tolerance)); + } + + /***************************************************/ + + [Description("Checks if a point lies on any of the input planar surfaces.")] + [Input("point", "Point to test.")] + [Input("surfaces", "Planar surfaces to test against.")] + [Input("tolerance", "Distance tolerance for the containment check.")] + [Output("isOnFace", "True if the point lies on at least one surface.")] + public static bool IsOnFace(this Point point, IEnumerable surfaces, double tolerance = Tolerance.Distance) + { + if (point == null || surfaces == null) + return false; + + return surfaces.Any(x => point.IsOnFace(x, tolerance)); + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static bool ContainsOnBoundary(ICurve boundary, Point point, Plane plane, bool acceptOnEdge, double tolerance) + { + if (boundary is Polyline polyline) + { + if (plane == null) + return false; + + return polyline.IsContaining(point, plane, acceptOnEdge, tolerance); + } + + return boundary.IIsContaining(new List { point }, acceptOnEdge, tolerance); + } + + /***************************************************/ + } +} diff --git a/Clipper_Engine/Compute/Offset.cs b/Clipper_Engine/Compute/Offset.cs index d611ac4..3f524e5 100644 --- a/Clipper_Engine/Compute/Offset.cs +++ b/Clipper_Engine/Compute/Offset.cs @@ -95,6 +95,42 @@ public static List Offset(this Polyline polyline, double distance = 0, } /***************************************************/ + + [Description("Offset a closed planar polyline inward or outward, returning the largest valid result. Flips offset direction when the result unexpectedly grows or shrinks.")] + [Input("polyline", "A BHoM Polyline representing the curve to offset.")] + [Input("distance", "The offset distance (positive magnitude).")] + [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the polyline.")] + [Input("inward", "If true, offsets inward; otherwise outward.")] + [Input("tolerance", "Tolerance to be used for planarity and closedness checks as well as for offset computations.")] + [Output("polyline", "The offset polyline with the largest area, or null if invalid.")] + public static Polyline Offset(this Polyline polyline, double distance, Plane curvePlane = null, bool inward = false, double tolerance = Tolerance.Distance) + { + if (polyline == null || distance == 0) + return polyline; + + if (curvePlane == null) + curvePlane = polyline.FitPlane(); + + double signedDistance = inward ? -distance : distance; + Polyline offset = Offset(polyline, signedDistance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); + if (offset == null || offset.ControlPoints.Count == 0) + return null; + + double originalDiagonal = polyline.Bounds().DiagonalLength(); + double offsetDiagonal = offset.Bounds().DiagonalLength(); + + if (inward && offsetDiagonal > originalDiagonal) + offset = Offset(polyline, distance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); + else if (!inward && offsetDiagonal < originalDiagonal) + offset = Offset(polyline, -distance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); + + if (offset != null && offset.ControlPoints.Any()) + return offset; + + return null; + } + + /***************************************************/ } } diff --git a/Clipper_Engine/Query/PolygonRelations.cs b/Clipper_Engine/Query/PolygonRelations.cs new file mode 100644 index 0000000..a5b10a3 --- /dev/null +++ b/Clipper_Engine/Query/PolygonRelations.cs @@ -0,0 +1,144 @@ +/* + * 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("Checks if two polylines intersect on a plane without one fully containing the other.")] + [Input("poly1", "First polyline.")] + [Input("poly2", "Second polyline.")] + [Input("plane", "Optional plane for the Clipper boolean intersection. If null, the plane is fitted from the first polyline.")] + [Input("tolerance", "Distance tolerance.")] + [Output("intersects", "True if the polylines intersect.")] + public static bool PolygonIntersects(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; + + if (HasContainmentRelationship(poly1, poly2, tolerance)) + return false; + + List intersection = Compute.BooleanIntersection(poly1, poly2, plane, tolerance); + return intersection != null && intersection.Count > 0; + } + + /***************************************************/ + + [Description("Checks if two polylines have the same control points, optionally after projecting both onto a plane.")] + [Input("pLine1", "First polyline.")] + [Input("pLine2", "Second polyline.")] + [Input("plane", "Optional plane for comparison. If null, control points are compared in 3D.")] + [Input("tolerance", "Distance tolerance.")] + [Output("overlaps", "True if the polylines overlap.")] + public static bool PolygonOverlaps(this Polyline pLine1, Polyline pLine2, Plane plane = null, double tolerance = Tolerance.Distance) + { + if (pLine1 == null || pLine2 == null) + return false; + + if (pLine1.ControlPoints.Count != pLine2.ControlPoints.Count) + return false; + + if (plane == null) + return HasSameControlPoints(pLine1, pLine2, tolerance); + + return HasSameControlPointsOnPlane(pLine1, pLine2, plane, tolerance); + } + + /***************************************************/ + + [Description("Checks if a polyline is self-intersecting, optionally after projecting it onto a plane.")] + [Input("pLine", "Polyline to check.")] + [Input("plane", "Optional plane for the check. If null, the polyline is tested in 3D.")] + [Output("isSelfIntersecting", "True if self-intersecting.")] + public static bool IsSelfIntersectingPolygon(this Polyline pLine, Plane plane = null) + { + if (pLine == null || pLine.ControlPoints.Count < 3) + return false; + + if (plane == null) + return pLine.IsSelfIntersecting(); + + Polyline projected = pLine.Project(plane); + return projected?.IsSelfIntersecting() ?? pLine.IsSelfIntersecting(); + } + + /***************************************************/ + + [Description("Checks if two polylines have the same control-point set within tolerance.")] + [Input("pLine1", "First polyline.")] + [Input("pLine2", "Second polyline.")] + [Input("tolerance", "Distance tolerance.")] + [Output("same", "True if every control point of the second polyline matches a control point of the first.")] + public static bool HasSameControlPoints(this Polyline pLine1, Polyline pLine2, double tolerance = Tolerance.Distance) + { + if (pLine1 == null || pLine2 == null) + return false; + + if (pLine1.ControlPoints.Count != pLine2.ControlPoints.Count) + return false; + + HashSet pointSet = new HashSet(pLine1.ControlPoints); + return pLine2.ControlPoints.All(point => pointSet.Any(p => p.IsEqual(point, tolerance))); + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static bool HasContainmentRelationship(Polyline poly1, Polyline poly2, double tolerance) + { + double extent1 = poly1.Bounds().DiagonalLength(); + double extent2 = poly2.Bounds().DiagonalLength(); + + if (extent1 > extent2 && poly1.IsContaining(poly2, true, tolerance)) + return true; + + if (extent2 > extent1 && poly2.IsContaining(poly1, true, tolerance)) + return true; + + return false; + } + + /***************************************************/ + + private static bool HasSameControlPointsOnPlane(Polyline pLine1, Polyline pLine2, Plane plane, double tolerance) + { + List points1 = pLine1.ControlPoints.Select(p => p.Project(plane)).ToList(); + List points2 = pLine2.ControlPoints.Select(p => p.Project(plane)).ToList(); + return HasSameControlPoints(new Polyline { ControlPoints = points1 }, new Polyline { ControlPoints = points2 }, tolerance); + } + + /***************************************************/ + } +} diff --git a/Clipper_Engine/Query/UniquePolylines.cs b/Clipper_Engine/Query/UniquePolylines.cs new file mode 100644 index 0000000..09d8054 --- /dev/null +++ b/Clipper_Engine/Query/UniquePolylines.cs @@ -0,0 +1,70 @@ +/* + * 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; +using System.Linq; + +namespace BH.Engine.Clipper +{ + public static partial class Query + { + /***************************************************/ + /**** Public methods ****/ + /***************************************************/ + + [Description("Filters polylines to return only unique ones, optionally checking for self-intersections and overlaps on a plane.")] + [Input("polylines", "The collection of polylines to filter.")] + [Input("clipPlane", "Optional plane for overlap and self-intersection detection.")] + [Input("checkSelfIntersection", "Whether to exclude self-intersecting polylines (defaults to false).")] + [Output("unique", "A list of unique non-overlapping polylines.")] + public static List UniquePolylines(this IEnumerable polylines, Plane clipPlane = null, bool checkSelfIntersection = false) + { + if (polylines == null) + return new List(); + + List input = polylines.ToList(); + if (input.Count < 2) + return input; + + List uniquePolylines = new List(); + + foreach (Polyline pLine in input) + { + if (checkSelfIntersection && pLine.IsSelfIntersectingPolygon(clipPlane)) + continue; + + if (uniquePolylines.Any(x => x.PolygonOverlaps(pLine, clipPlane)) == false) + uniquePolylines.Add(pLine); + } + + if (uniquePolylines.Any()) + return uniquePolylines; + + return input; + } + + /***************************************************/ + } +} From cd1094c3c09eadf92ae2bbc94e26dd3debcd4c24 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Sun, 18 Oct 2026 22:51:26 +0200 Subject: [PATCH 02/14] Fix plane containment and difference --- Clipper_Engine/Compute/BooleanDifference.cs | 13 ++++++------- Clipper_Engine/Compute/IsContaining.cs | 4 ++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index 4f0fc41..07f0c21 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -37,9 +37,9 @@ 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("refRegions", "List of reference polylines to subtract from the main region. Regions off the subject plane are ignored.")] [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the main region.")] [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).")] @@ -62,11 +62,10 @@ public static List BooleanDifference(this Polyline region, List x.ControlPoints.Any(y => !y.IsInPlane(curvePlane, 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(curvePlane, tolerance))).ToList(); + if (clipRegions.Count == 0) + return new List { region }; // Find the orientation matrix to the global XY plane TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index f96b4d7..78f023c 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -176,6 +176,10 @@ private static bool AllPointsInPolygon(Polyline outer, IEnumerable points if (point == null) return false; + // A point farther than tolerance from the face is outside. Projection is only for the 2D test. + if (!point.IsInPlane(curvePlane, tolerance)) + return false; + Point ptOnXY = point.Project(curvePlane).Transform(orientation); PointInPolygonResult result = Clipper2Lib.Clipper.PointInPolygon(ptOnXY.ToPoint64(scale), path); if (result == PointInPolygonResult.IsOutside) From 9a3ddf43cc8dafd141cfe8104852519e6f10f6e8 Mon Sep 17 00:00:00 2001 From: Pawel Baran Date: Mon, 19 Oct 2026 16:41:44 +0200 Subject: [PATCH 03/14] suggested cleanup --- Clipper_Engine/Compute/IsOnFace.cs | 55 ++++++++-------- Clipper_Engine/Compute/Offset.cs | 2 + Clipper_Engine/Query/PolygonRelations.cs | 82 ++++-------------------- Clipper_Engine/Query/UniquePolylines.cs | 18 ++---- 4 files changed, 47 insertions(+), 110 deletions(-) diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs index b741255..d8b2042 100644 --- a/Clipper_Engine/Compute/IsOnFace.cs +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -45,46 +45,49 @@ public static bool IsOnFace(this Point point, PlanarSurface surface, double tole if (point == null || surface?.ExternalBoundary == null) return false; - Plane plane = surface.FitPlane(tolerance); - if (!ContainsOnBoundary(surface.ExternalBoundary, point, plane, true, tolerance)) + //TODO: check if all outlines are polyline + Polyline outer = surface.ExternalBoundary as Polyline; + if (outer == null) + { + BH.Engine.Base.Compute.RecordError("Clipper IsOnFace method only works for planar surfaces with polyline boundaries."); return false; + } - if (surface.InternalBoundaries == null) - return true; - - return !surface.InternalBoundaries.Any(x => x != null && ContainsOnBoundary(x, point, plane, false, tolerance)); - } - - /***************************************************/ - - [Description("Checks if a point lies on any of the input planar surfaces.")] - [Input("point", "Point to test.")] - [Input("surfaces", "Planar surfaces to test against.")] - [Input("tolerance", "Distance tolerance for the containment check.")] - [Output("isOnFace", "True if the point lies on at least one surface.")] - public static bool IsOnFace(this Point point, IEnumerable surfaces, double tolerance = Tolerance.Distance) - { - if (point == null || surfaces == null) + List inner = surface.InternalBoundaries?.Select(x => x as Polyline).Where(x => x != null).ToList() ?? new List(); + if (inner.Count != surface.InternalBoundaries?.Count) + { + BH.Engine.Base.Compute.RecordError("Clipper IsOnFace method only works for planar surfaces with polyline boundaries."); return false; + } + + Plane plane = surface.FitPlane(tolerance); + return point.IsOnFace(outer, inner, plane, tolerance); - return surfaces.Any(x => point.IsOnFace(x, tolerance)); } - /***************************************************/ - /**** Private methods ****/ /***************************************************/ - private static bool ContainsOnBoundary(ICurve boundary, Point point, Plane plane, bool acceptOnEdge, double 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 curvePlane = null, double tolerance = Tolerance.Distance) { - if (boundary is Polyline polyline) + if (curvePlane == null) { - if (plane == null) + curvePlane = outer?.FitPlane(tolerance); + if (outer != null && (outer.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance)) || inner.Any(y => y.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))))) + { + Base.Compute.RecordError("Clipper IsOnFace method only works for coplanar polylines."); return false; + } - return polyline.IsContaining(point, plane, acceptOnEdge, tolerance); + if (!point.IsInPlane(curvePlane, tolerance) == false) + return false; } - return boundary.IIsContaining(new List { point }, acceptOnEdge, tolerance); + return outer.IsContaining(point, curvePlane, true, tolerance) && !inner.Any(x => x != null && x.IsContaining(point, curvePlane, false, tolerance)); } /***************************************************/ diff --git a/Clipper_Engine/Compute/Offset.cs b/Clipper_Engine/Compute/Offset.cs index 3f524e5..f57dfc8 100644 --- a/Clipper_Engine/Compute/Offset.cs +++ b/Clipper_Engine/Compute/Offset.cs @@ -105,6 +105,8 @@ public static List Offset(this Polyline polyline, double distance = 0, [Output("polyline", "The offset polyline with the largest area, or null if invalid.")] public static Polyline Offset(this Polyline polyline, double distance, Plane curvePlane = null, bool inward = false, double tolerance = Tolerance.Distance) { + //TODO: this is a wrapper, not foundational method, move it to the client + if (polyline == null || distance == 0) return polyline; diff --git a/Clipper_Engine/Query/PolygonRelations.cs b/Clipper_Engine/Query/PolygonRelations.cs index a5b10a3..d52266f 100644 --- a/Clipper_Engine/Query/PolygonRelations.cs +++ b/Clipper_Engine/Query/PolygonRelations.cs @@ -41,14 +41,11 @@ public static partial class Query [Input("plane", "Optional plane for the Clipper boolean intersection. If null, the plane is fitted from the first polyline.")] [Input("tolerance", "Distance tolerance.")] [Output("intersects", "True if the polylines intersect.")] - public static bool PolygonIntersects(this Polyline poly1, Polyline poly2, Plane plane = null, double tolerance = Tolerance.Distance) + public static bool DoesIntersect(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; - if (HasContainmentRelationship(poly1, poly2, tolerance)) - return false; - List intersection = Compute.BooleanIntersection(poly1, poly2, plane, tolerance); return intersection != null && intersection.Count > 0; } @@ -61,7 +58,7 @@ public static bool PolygonIntersects(this Polyline poly1, Polyline poly2, Plane [Input("plane", "Optional plane for comparison. If null, control points are compared in 3D.")] [Input("tolerance", "Distance tolerance.")] [Output("overlaps", "True if the polylines overlap.")] - public static bool PolygonOverlaps(this Polyline pLine1, Polyline pLine2, Plane plane = null, double tolerance = Tolerance.Distance) + public static bool IsSame(this Polyline pLine1, Polyline pLine2, Plane plane = null, double tolerance = Tolerance.Distance) { if (pLine1 == null || pLine2 == null) return false; @@ -69,74 +66,17 @@ public static bool PolygonOverlaps(this Polyline pLine1, Polyline pLine2, Plane if (pLine1.ControlPoints.Count != pLine2.ControlPoints.Count) return false; - if (plane == null) - return HasSameControlPoints(pLine1, pLine2, tolerance); - - return HasSameControlPointsOnPlane(pLine1, pLine2, plane, tolerance); - } - - /***************************************************/ + List points1 = pLine1.ControlPoints; + List points2 = pLine2.ControlPoints; - [Description("Checks if a polyline is self-intersecting, optionally after projecting it onto a plane.")] - [Input("pLine", "Polyline to check.")] - [Input("plane", "Optional plane for the check. If null, the polyline is tested in 3D.")] - [Output("isSelfIntersecting", "True if self-intersecting.")] - public static bool IsSelfIntersectingPolygon(this Polyline pLine, Plane plane = null) - { - if (pLine == null || pLine.ControlPoints.Count < 3) - return false; + if (plane != null) + { + points1 = points1.Select(p => p.Project(plane)).ToList(); + points2 = points2.Select(p => p.Project(plane)).ToList(); + } - if (plane == null) - return pLine.IsSelfIntersecting(); - - Polyline projected = pLine.Project(plane); - return projected?.IsSelfIntersecting() ?? pLine.IsSelfIntersecting(); - } - - /***************************************************/ - - [Description("Checks if two polylines have the same control-point set within tolerance.")] - [Input("pLine1", "First polyline.")] - [Input("pLine2", "Second polyline.")] - [Input("tolerance", "Distance tolerance.")] - [Output("same", "True if every control point of the second polyline matches a control point of the first.")] - public static bool HasSameControlPoints(this Polyline pLine1, Polyline pLine2, double tolerance = Tolerance.Distance) - { - if (pLine1 == null || pLine2 == null) - return false; - - if (pLine1.ControlPoints.Count != pLine2.ControlPoints.Count) - return false; - - HashSet pointSet = new HashSet(pLine1.ControlPoints); - return pLine2.ControlPoints.All(point => pointSet.Any(p => p.IsEqual(point, tolerance))); - } - - /***************************************************/ - /**** Private methods ****/ - /***************************************************/ - - private static bool HasContainmentRelationship(Polyline poly1, Polyline poly2, double tolerance) - { - double extent1 = poly1.Bounds().DiagonalLength(); - double extent2 = poly2.Bounds().DiagonalLength(); - - if (extent1 > extent2 && poly1.IsContaining(poly2, true, tolerance)) - return true; - - if (extent2 > extent1 && poly2.IsContaining(poly1, true, tolerance)) - return true; - - return false; - } - - /***************************************************/ - - private static bool HasSameControlPointsOnPlane(Polyline pLine1, Polyline pLine2, Plane plane, double tolerance) - { - List points1 = pLine1.ControlPoints.Select(p => p.Project(plane)).ToList(); - List points2 = pLine2.ControlPoints.Select(p => p.Project(plane)).ToList(); - return HasSameControlPoints(new Polyline { ControlPoints = points1 }, new Polyline { ControlPoints = points2 }, tolerance); + //TODO: in points2, find all indices of points1[0] + //TODO: for each index, check if the sequence of points matches points1, considering wrap-around and tolerance } /***************************************************/ diff --git a/Clipper_Engine/Query/UniquePolylines.cs b/Clipper_Engine/Query/UniquePolylines.cs index 09d8054..834cf38 100644 --- a/Clipper_Engine/Query/UniquePolylines.cs +++ b/Clipper_Engine/Query/UniquePolylines.cs @@ -44,25 +44,17 @@ public static List UniquePolylines(this IEnumerable polyline if (polylines == null) return new List(); - List input = polylines.ToList(); - if (input.Count < 2) - return input; - List uniquePolylines = new List(); - - foreach (Polyline pLine in input) + foreach (Polyline polyline in polylines) { - if (checkSelfIntersection && pLine.IsSelfIntersectingPolygon(clipPlane)) + if (polyline == null) continue; - if (uniquePolylines.Any(x => x.PolygonOverlaps(pLine, clipPlane)) == false) - uniquePolylines.Add(pLine); + if (!uniquePolylines.Any(x => x.IsSame(polyline))) + uniquePolylines.Add(polyline); } - if (uniquePolylines.Any()) - return uniquePolylines; - - return input; + return uniquePolylines; } /***************************************************/ From 522e93633f87bbefd3488253a715be29a6983a9d Mon Sep 17 00:00:00 2001 From: Pawel Baran Date: Mon, 19 Oct 2026 16:47:57 +0200 Subject: [PATCH 04/14] is containing cleaned up --- Clipper_Engine/Compute/IsContaining.cs | 182 +++++++++++++------------ 1 file changed, 95 insertions(+), 87 deletions(-) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 78f023c..61400de 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -37,22 +37,59 @@ public static partial class Compute /**** Public methods ****/ /***************************************************/ - [Description("Checks if all input points lie inside a polyline on the given plane, using Clipper2 point-in-polygon.")] - [Input("outer", "Closed polyline region.")] - [Input("points", "Points to test.")] - [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] - [Input("acceptOnEdge", "If true, points on the boundary are accepted.")] - [Input("tolerance", "Distance tolerance used for orientation and scaling.")] - [Output("isContaining", "True if every point is inside the polyline.")] - public static bool IsContaining(this Polyline outer, List points, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + [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.")] + [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) { - if (outer == null || points == null || curvePlane == null || outer.ControlPoints.Count < 3) + if (region == null || points == null || region.ControlPoints.Count < 3 || points.Count == 0) return false; - if (points.Count == 0) - return true; + if (curvePlane == null) + { + curvePlane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, 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; + } + + // Find the orientation matrix to the global XY plane + TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); + if (orientation == null) + return false; + + // Transform outer polyline to the global XY plane + Polyline regionOnXY = region.OpenPolylineOnXY(orientation); + + // Scale is set to run the containment test at a much larger scale for precision + double scale = 1 / tolerance; + Path64 regionPath = regionOnXY.ToClipPath(scale); - return AllPointsInPolygon(outer, points, curvePlane, acceptOnEdge, tolerance); + // Transform and test each point + foreach (Point pnt in points) + { + Point64 checkPoint = pnt.Transform(orientation).ToPoint64(scale); + PointInPolygonResult checkResult = Clipper2Lib.Clipper.PointInPolygon(checkPoint, regionPath); + + if (checkResult == PointInPolygonResult.IsOutside) + return false; + + // Check if point is on edge, but we don't accept edge hits + if (!acceptOnEdge && checkResult == PointInPolygonResult.IsOn) + return false; + } + + return true; } /***************************************************/ @@ -69,24 +106,7 @@ public static bool IsContaining(this Polyline outer, Point point, Plane curvePla if (point == null) return false; - return AllPointsInPolygon(outer, new List { point }, curvePlane, acceptOnEdge, tolerance); - } - - /***************************************************/ - - [Description("Checks if a polyline is contained in another polyline on the given plane, using Clipper2 point-in-polygon on the inner control points.")] - [Input("outer", "Closed polyline region.")] - [Input("inner", "Polyline to test.")] - [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] - [Input("acceptOnEdge", "If true, inner vertices on the outer boundary are accepted.")] - [Input("tolerance", "Distance tolerance used for orientation and scaling.")] - [Output("isContaining", "True if every control point of the inner polyline is inside the outer polyline.")] - public static bool IsContaining(this Polyline outer, Polyline inner, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) - { - if (outer == null || inner == null || curvePlane == null) - return false; - - return AllPointsInPolygon(outer, inner.ControlPoints, curvePlane, acceptOnEdge, tolerance); + return outer.IsContaining(new List { point }, curvePlane, acceptOnEdge, tolerance); } /***************************************************/ @@ -104,88 +124,75 @@ public static bool IsContaining(this Polyline outer, Line line, Plane curvePlane return false; Point mid = (line.Start + line.End) * 0.5; - return AllPointsInPolygon(outer, new List { line.Start, line.End, mid }, curvePlane, acceptOnEdge, tolerance); + return outer.IsContaining(new List { line.Start, line.End, mid }, curvePlane, acceptOnEdge, tolerance); } /***************************************************/ - [Description("Checks if every input point is contained by at least one polyline on the given plane.")] - [Input("regions", "Closed polyline regions.")] - [Input("points", "Points to test.")] - [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] - [Input("acceptOnEdge", "If true, points on a boundary are accepted.")] - [Input("tolerance", "Distance tolerance used for orientation and scaling.")] - [Output("isContaining", "True if every point lies inside at least one region.")] - public static bool IsContaining(this List regions, List points, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + [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("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) { - if (regions == null || points == null || curvePlane == null) + if (region == null || refRegion == null || region.ControlPoints.Count < 3 || refRegion.ControlPoints.Count < 3) return false; - List remainingPoints = new List(points); - foreach (Polyline pLine in regions) + if (curvePlane == null) { - if (!remainingPoints.Any()) - break; + curvePlane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); + return false; + } - for (int i = remainingPoints.Count - 1; i >= 0; i--) + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) { - if (pLine.IsContaining(remainingPoints[i], curvePlane, acceptOnEdge, tolerance)) - remainingPoints.RemoveAt(i); + Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); + return false; } } - return remainingPoints.Count == 0; - } - - /***************************************************/ - - [Description("Checks if the input point is contained by at least one polyline on the given plane.")] - [Input("regions", "Closed polyline regions.")] - [Input("point", "Point to test.")] - [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper test.")] - [Input("acceptOnEdge", "If true, a point on a boundary is accepted.")] - [Input("tolerance", "Distance tolerance used for orientation and scaling.")] - [Output("isContaining", "True if the point lies inside at least one region.")] - public static bool IsContaining(this List regions, Point point, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) - { - if (regions == null || point == null || curvePlane == null) + if (region.IsContaining(refRegion.ControlPoints, curvePlane, true, tolerance) == false) return false; - return regions.Any(x => x.IsContaining(point, curvePlane, acceptOnEdge, tolerance)); + //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; } - /***************************************************/ - /**** Private methods ****/ /***************************************************/ - private static bool AllPointsInPolygon(Polyline outer, IEnumerable points, Plane curvePlane, bool acceptOnEdge, double tolerance) + [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("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) { - TransformMatrix orientation = outer.OrientationToGlobalXY(curvePlane, tolerance); - if (orientation == null) - return false; - - Polyline outerOnXY = outer.OpenPolylineOnXY(orientation, tolerance); - if (outerOnXY.ControlPoints.Count < 3) - return false; - - double scale = 1 / tolerance; - Path64 path = outerOnXY.ToClipPath(scale); - - foreach (Point point in points) + if (curvePlane == null) { - if (point == null) - return false; - - // A point farther than tolerance from the face is outside. Projection is only for the 2D test. - if (!point.IsInPlane(curvePlane, tolerance)) + curvePlane = region.FitPlane(); + if (region.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; + } + } - Point ptOnXY = point.Project(curvePlane).Transform(orientation); - PointInPolygonResult result = Clipper2Lib.Clipper.PointInPolygon(ptOnXY.ToPoint64(scale), path); - if (result == PointInPolygonResult.IsOutside) + foreach (Polyline refRegion in refRegions) + { + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; + } - if (result == PointInPolygonResult.IsOn && !acceptOnEdge) + if (!region.IsContaining(refRegion, curvePlane, tolerance)) return false; } @@ -195,3 +202,4 @@ private static bool AllPointsInPolygon(Polyline outer, IEnumerable points /***************************************************/ } } + From b599804463301edd94f0d1c9ede77c062961637b Mon Sep 17 00:00:00 2001 From: Pawel Baran Date: Mon, 19 Oct 2026 16:50:15 +0200 Subject: [PATCH 05/14] boolean difference cleaned up --- Clipper_Engine/Compute/BooleanDifference.cs | 37 +++------------------ 1 file changed, 5 insertions(+), 32 deletions(-) diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index 07f0c21..b033067 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -60,12 +60,12 @@ public static List BooleanDifference(this Polyline region, List x.ControlPoints.All(y => y != null && y.IsInPlane(curvePlane, tolerance))).ToList(); - if (clipRegions.Count == 0) - return new List { region }; + // 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(curvePlane, tolerance))).ToList(); + if (clipRegions.Count == 0) + return new List { region }; + } // Find the orientation matrix to the global XY plane TransformMatrix orientation = region.OrientationToGlobalXY(curvePlane, tolerance); @@ -100,33 +100,6 @@ public static List BooleanDifference(this Polyline region, List BooleanDifference(this List subjects, List clips, Plane curvePlane = null, double tolerance = Tolerance.Distance) - { - if (subjects == null || !subjects.Any()) - return subjects ?? new List(); - - if (clips == null || !clips.Any()) - return subjects; - - List result = new List(); - foreach (Polyline subject in subjects) - { - List difference = subject.BooleanDifference(clips, curvePlane, tolerance); - if (difference != null) - result.AddRange(difference); - } - - return result; - } - - /***************************************************/ } } From 3685b4de4ec523350a9e3ff2b47a1b3462ce748d Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Wed, 21 Oct 2026 13:20:37 +0200 Subject: [PATCH 06/14] Plane rename and methods migration --- Clipper_Engine/Compute/BooleanDifference.cs | 14 ++--- Clipper_Engine/Compute/BooleanIntersection.cs | 22 +++---- Clipper_Engine/Compute/BooleanUnion.cs | 12 ++-- Clipper_Engine/Compute/IsContaining.cs | 59 +++++++++---------- Clipper_Engine/Compute/IsOnFace.cs | 57 +++++++++++++----- Clipper_Engine/Compute/Offset.cs | 50 ++-------------- Clipper_Engine/Query/DoesIntersect.cs | 53 +++++++++++++++++ .../Query/{PolygonRelations.cs => IsSame.cs} | 45 +++++++------- Clipper_Engine/Query/UniquePolylines.cs | 6 +- 9 files changed, 181 insertions(+), 137 deletions(-) create mode 100644 Clipper_Engine/Query/DoesIntersect.cs rename Clipper_Engine/Query/{PolygonRelations.cs => IsSame.cs} (67%) diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index b033067..ee13739 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -40,10 +40,10 @@ public static partial class Compute [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. Regions off the subject plane are ignored.")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the main region.")] + [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,23 +52,23 @@ 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(curvePlane, tolerance))).ToList(); + clipRegions = clipRegions.Where(x => x.ControlPoints.All(y => y != null && y.IsInPlane(plane, tolerance))).ToList(); if (clipRegions.Count == 0) 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..03e401b 100644 --- a/Clipper_Engine/Compute/BooleanIntersection.cs +++ b/Clipper_Engine/Compute/BooleanIntersection.cs @@ -40,32 +40,32 @@ 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 (plane == null) { - curvePlane = poly1.FitPlane(); - if (poly1.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + 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..a194f64 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,10 @@ public static List BooleanUnion(this List polylines, Plane c if (validPolylines.Count == 1) return new List { validPolylines[0] }; - if (curvePlane == null) + if (plane == null) { - curvePlane = validPolylines[0].FitPlane(); - if (validPolylines.Any(x => x.ControlPoints.Any(y => !y.IsInPlane(curvePlane, tolerance)))) + 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 +66,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/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 61400de..2c11763 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -40,31 +40,30 @@ 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("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 (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))) + 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; @@ -97,16 +96,16 @@ 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("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper 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 curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline outer, Point point, Plane plane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { if (point == null) return false; - return outer.IsContaining(new List { point }, curvePlane, acceptOnEdge, tolerance); + return outer.IsContaining(new List { point }, plane, acceptOnEdge, tolerance); } /***************************************************/ @@ -114,17 +113,17 @@ public static bool IsContaining(this Polyline outer, Point point, Plane curvePla [Description("Checks if a line is contained in a polyline on the given plane, using Clipper2 point-in-polygon on the line start, end, and midpoint.")] [Input("outer", "Closed polyline region.")] [Input("line", "Line to test.")] - [Input("curvePlane", "Work plane. Geometry is projected onto this plane before the Clipper 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, points on the boundary are accepted.")] [Input("tolerance", "Distance tolerance used for orientation and scaling.")] [Output("isContaining", "True if the line start, end, and midpoint are inside the polyline.")] - public static bool IsContaining(this Polyline outer, Line line, Plane curvePlane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline outer, Line line, Plane plane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { - if (outer == null || line == null || curvePlane == null) + if (outer == null || line == null || plane == null) return false; Point mid = (line.Start + line.End) * 0.5; - return outer.IsContaining(new List { line.Start, line.End, mid }, curvePlane, acceptOnEdge, tolerance); + return outer.IsContaining(new List { line.Start, line.End, mid }, plane, acceptOnEdge, tolerance); } /***************************************************/ @@ -133,35 +132,35 @@ public static bool IsContaining(this Polyline outer, Line line, Plane curvePlane "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 (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))) + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } } - if (region.IsContaining(refRegion.ControlPoints, curvePlane, true, tolerance) == false) + if (region.IsContaining(refRegion.ControlPoints, plane, true, tolerance) == false) 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; } /***************************************************/ @@ -169,15 +168,15 @@ 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 (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; @@ -186,13 +185,13 @@ public static bool IsContaining(this Polyline region, List refRegions, foreach (Polyline refRegion in refRegions) { - if (refRegion.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))) + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } - if (!region.IsContaining(refRegion, curvePlane, tolerance)) + if (!region.IsContaining(refRegion, plane, tolerance)) return false; } diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs index d8b2042..9e1bf93 100644 --- a/Clipper_Engine/Compute/IsOnFace.cs +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -45,22 +45,16 @@ public static bool IsOnFace(this Point point, PlanarSurface surface, double tole if (point == null || surface?.ExternalBoundary == null) return false; - //TODO: check if all outlines are polyline - Polyline outer = surface.ExternalBoundary as Polyline; - if (outer == null) + if (!TryGetPolylineBoundaries(surface, out Polyline outer, out List inner)) { BH.Engine.Base.Compute.RecordError("Clipper IsOnFace method only works for planar surfaces with polyline boundaries."); return false; } - List inner = surface.InternalBoundaries?.Select(x => x as Polyline).Where(x => x != null).ToList() ?? new List(); - if (inner.Count != surface.InternalBoundaries?.Count) - { - BH.Engine.Base.Compute.RecordError("Clipper IsOnFace method only works for planar surfaces with polyline boundaries."); + Plane plane = surface.FitPlane(tolerance); + if (!point.IsInPlane(plane, tolerance)) return false; - } - Plane plane = surface.FitPlane(tolerance); return point.IsOnFace(outer, inner, plane, tolerance); } @@ -72,22 +66,55 @@ public static bool IsOnFace(this Point point, PlanarSurface surface, double tole //[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 curvePlane = null, double tolerance = Tolerance.Distance) + public static bool IsOnFace(this Point point, Polyline outer, List inner, Plane plane = null, double tolerance = Tolerance.Distance) { - if (curvePlane == null) + if (point == null || outer == null) + return false; + + if (plane == null) { - curvePlane = outer?.FitPlane(tolerance); - if (outer != null && (outer.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance)) || inner.Any(y => y.ControlPoints.Any(x => !x.IsInPlane(curvePlane, tolerance))))) + 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 coplanar polylines."); return false; } - if (!point.IsInPlane(curvePlane, tolerance) == false) + if (!point.IsInPlane(plane, tolerance)) return false; } - return outer.IsContaining(point, curvePlane, true, tolerance) && !inner.Any(x => x != null && x.IsContaining(point, curvePlane, false, tolerance)); + return outer.IsContaining(point, plane, true, tolerance) && !inner.Any(x => x != null && x.IsContaining(point, plane, false, tolerance)); + } + + /***************************************************/ + /**** Private methods ****/ + /***************************************************/ + + private static bool TryGetPolylineBoundaries(PlanarSurface surface, out Polyline outer, out List inner) + { + outer = null; + inner = new List(); + + Polyline externalPolyline = surface.ExternalBoundary as Polyline; + if (externalPolyline == null) + return false; + + outer = externalPolyline; + + if (surface.InternalBoundaries == null) + return true; + + foreach (ICurve boundary in surface.InternalBoundaries) + { + Polyline innerPolyline = boundary as Polyline; + if (innerPolyline == null) + return false; + + inner.Add(innerPolyline); + } + + return true; } /***************************************************/ diff --git a/Clipper_Engine/Compute/Offset.cs b/Clipper_Engine/Compute/Offset.cs index f57dfc8..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; @@ -95,44 +95,6 @@ public static List Offset(this Polyline polyline, double distance = 0, } /***************************************************/ - - [Description("Offset a closed planar polyline inward or outward, returning the largest valid result. Flips offset direction when the result unexpectedly grows or shrinks.")] - [Input("polyline", "A BHoM Polyline representing the curve to offset.")] - [Input("distance", "The offset distance (positive magnitude).")] - [Input("curvePlane", "Optional plane for the geometry. If null, will be fitted from the polyline.")] - [Input("inward", "If true, offsets inward; otherwise outward.")] - [Input("tolerance", "Tolerance to be used for planarity and closedness checks as well as for offset computations.")] - [Output("polyline", "The offset polyline with the largest area, or null if invalid.")] - public static Polyline Offset(this Polyline polyline, double distance, Plane curvePlane = null, bool inward = false, double tolerance = Tolerance.Distance) - { - //TODO: this is a wrapper, not foundational method, move it to the client - - if (polyline == null || distance == 0) - return polyline; - - if (curvePlane == null) - curvePlane = polyline.FitPlane(); - - double signedDistance = inward ? -distance : distance; - Polyline offset = Offset(polyline, signedDistance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); - if (offset == null || offset.ControlPoints.Count == 0) - return null; - - double originalDiagonal = polyline.Bounds().DiagonalLength(); - double offsetDiagonal = offset.Bounds().DiagonalLength(); - - if (inward && offsetDiagonal > originalDiagonal) - offset = Offset(polyline, distance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); - else if (!inward && offsetDiagonal < originalDiagonal) - offset = Offset(polyline, -distance, curvePlane, tolerance).OrderByDescending(x => x.Area()).FirstOrDefault(); - - if (offset != null && offset.ControlPoints.Any()) - return offset; - - return null; - } - - /***************************************************/ } } diff --git a/Clipper_Engine/Query/DoesIntersect.cs b/Clipper_Engine/Query/DoesIntersect.cs new file mode 100644 index 0000000..292b963 --- /dev/null +++ b/Clipper_Engine/Query/DoesIntersect.cs @@ -0,0 +1,53 @@ +/* + * 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; +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 DoesIntersect(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/Query/PolygonRelations.cs b/Clipper_Engine/Query/IsSame.cs similarity index 67% rename from Clipper_Engine/Query/PolygonRelations.cs rename to Clipper_Engine/Query/IsSame.cs index d52266f..1d88b84 100644 --- a/Clipper_Engine/Query/PolygonRelations.cs +++ b/Clipper_Engine/Query/IsSame.cs @@ -35,27 +35,10 @@ 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", "Optional plane for the Clipper boolean intersection. If null, the plane is fitted from the first polyline.")] - [Input("tolerance", "Distance tolerance.")] - [Output("intersects", "True if the polylines intersect.")] - public static bool DoesIntersect(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; - } - - /***************************************************/ - - [Description("Checks if two polylines have the same control points, optionally after projecting both onto a plane.")] + [Description("Checks if two polylines have the same control points. If plane is null, comparison is in 2D. If plane is not null, 3D control points are projected onto the plane before comparison.")] [Input("pLine1", "First polyline.")] [Input("pLine2", "Second polyline.")] - [Input("plane", "Optional plane for comparison. If null, control points are compared in 3D.")] + [Input("plane", "If null, comparison is in 2D (control points compared directly). If not null, 3D control points are projected onto this plane before comparison.")] [Input("tolerance", "Distance tolerance.")] [Output("overlaps", "True if the polylines overlap.")] public static bool IsSame(this Polyline pLine1, Polyline pLine2, Plane plane = null, double tolerance = Tolerance.Distance) @@ -75,8 +58,28 @@ public static bool IsSame(this Polyline pLine1, Polyline pLine2, Plane plane = n points2 = points2.Select(p => p.Project(plane)).ToList(); } - //TODO: in points2, find all indices of points1[0] - //TODO: for each index, check if the sequence of points matches points1, considering wrap-around and tolerance + int count = points1.Count; + + for (int i = 0; i < count; i++) + { + if (!points2[i].IsEqual(points1[0], tolerance)) + continue; + + bool matches = true; + for (int j = 0; j < count; j++) + { + if (!points1[j].IsEqual(points2[(i + j) % count], tolerance)) + { + matches = false; + break; + } + } + + if (matches) + return true; + } + + return false; } /***************************************************/ diff --git a/Clipper_Engine/Query/UniquePolylines.cs b/Clipper_Engine/Query/UniquePolylines.cs index 834cf38..7d53054 100644 --- a/Clipper_Engine/Query/UniquePolylines.cs +++ b/Clipper_Engine/Query/UniquePolylines.cs @@ -36,10 +36,10 @@ public static partial class Query [Description("Filters polylines to return only unique ones, optionally checking for self-intersections and overlaps on a plane.")] [Input("polylines", "The collection of polylines to filter.")] - [Input("clipPlane", "Optional plane for overlap and self-intersection detection.")] + [Input("plane", "If null, comparison is in 2D (control points compared directly). If not null, 3D control points are projected onto this plane before comparison.")] [Input("checkSelfIntersection", "Whether to exclude self-intersecting polylines (defaults to false).")] [Output("unique", "A list of unique non-overlapping polylines.")] - public static List UniquePolylines(this IEnumerable polylines, Plane clipPlane = null, bool checkSelfIntersection = false) + public static List UniquePolylines(this IEnumerable polylines, Plane plane = null, bool checkSelfIntersection = false) { if (polylines == null) return new List(); @@ -50,7 +50,7 @@ public static List UniquePolylines(this IEnumerable polyline if (polyline == null) continue; - if (!uniquePolylines.Any(x => x.IsSame(polyline))) + if (!uniquePolylines.Any(x => x.IsSame(polyline, plane))) uniquePolylines.Add(polyline); } From b74acd4ff08e4cc823b139623ff1eb13cb593229 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Wed, 21 Oct 2026 17:06:49 +0200 Subject: [PATCH 07/14] Input and RecordError tweaks --- Clipper_Engine/Compute/IsContaining.cs | 8 ++++---- Clipper_Engine/Compute/IsOnFace.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 2c11763..72b2076 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -100,7 +100,7 @@ public static bool IsContaining(this Polyline region, List points, Plane [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, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline outer, Point point, Plane plane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { if (point == null) return false; @@ -117,7 +117,7 @@ public static bool IsContaining(this Polyline outer, Point point, Plane plane, b [Input("acceptOnEdge", "If true, points on the boundary are accepted.")] [Input("tolerance", "Distance tolerance used for orientation and scaling.")] [Output("isContaining", "True if the line start, end, and midpoint are inside the polyline.")] - public static bool IsContaining(this Polyline outer, Line line, Plane plane, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) + public static bool IsContaining(this Polyline outer, Line line, Plane plane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) { if (outer == null || line == null || plane == null) return false; @@ -151,7 +151,7 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); + Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); return false; } } @@ -187,7 +187,7 @@ public static bool IsContaining(this Polyline region, List refRegions, { if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { - Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); + Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); return false; } diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs index 9e1bf93..a67fefc 100644 --- a/Clipper_Engine/Compute/IsOnFace.cs +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -76,7 +76,7 @@ public static bool IsOnFace(this Point point, Polyline outer, List inn 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 coplanar polylines."); + Base.Compute.RecordError("Clipper IsOnFace method only works for planar polylines."); return false; } From 2eecaa1cecea1494609dc727f0822f3a4638eb27 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Thu, 22 Oct 2026 12:20:08 +0200 Subject: [PATCH 08/14] Renaming --- .../{UniquePolylines.cs => CullDuplicates.cs} | 20 ++--- .../{DoesIntersect.cs => DoesOverlap.cs} | 3 +- Clipper_Engine/Query/IsSame.cs | 87 ------------------- 3 files changed, 11 insertions(+), 99 deletions(-) rename Clipper_Engine/Query/{UniquePolylines.cs => CullDuplicates.cs} (67%) rename Clipper_Engine/Query/{DoesIntersect.cs => DoesOverlap.cs} (94%) delete mode 100644 Clipper_Engine/Query/IsSame.cs diff --git a/Clipper_Engine/Query/UniquePolylines.cs b/Clipper_Engine/Query/CullDuplicates.cs similarity index 67% rename from Clipper_Engine/Query/UniquePolylines.cs rename to Clipper_Engine/Query/CullDuplicates.cs index 7d53054..391669a 100644 --- a/Clipper_Engine/Query/UniquePolylines.cs +++ b/Clipper_Engine/Query/CullDuplicates.cs @@ -20,6 +20,7 @@ * along with this code. If not, see . */ +using BH.Engine.Geometry; using BH.oM.Base.Attributes; using BH.oM.Geometry; using System.Collections.Generic; @@ -34,27 +35,26 @@ public static partial class Query /**** Public methods ****/ /***************************************************/ - [Description("Filters polylines to return only unique ones, optionally checking for self-intersections and overlaps on a plane.")] - [Input("polylines", "The collection of polylines to filter.")] - [Input("plane", "If null, comparison is in 2D (control points compared directly). If not null, 3D control points are projected onto this plane before comparison.")] - [Input("checkSelfIntersection", "Whether to exclude self-intersecting polylines (defaults to false).")] - [Output("unique", "A list of unique non-overlapping polylines.")] - public static List UniquePolylines(this IEnumerable polylines, Plane plane = null, bool checkSelfIntersection = false) + [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 uniquePolylines = new List(); + List culled = new List(); foreach (Polyline polyline in polylines) { if (polyline == null) continue; - if (!uniquePolylines.Any(x => x.IsSame(polyline, plane))) - uniquePolylines.Add(polyline); + if (!culled.Any(x => x.IsSame(polyline, plane))) + culled.Add(polyline); } - return uniquePolylines; + return culled; } /***************************************************/ diff --git a/Clipper_Engine/Query/DoesIntersect.cs b/Clipper_Engine/Query/DoesOverlap.cs similarity index 94% rename from Clipper_Engine/Query/DoesIntersect.cs rename to Clipper_Engine/Query/DoesOverlap.cs index 292b963..fda1ea1 100644 --- a/Clipper_Engine/Query/DoesIntersect.cs +++ b/Clipper_Engine/Query/DoesOverlap.cs @@ -20,7 +20,6 @@ * along with this code. If not, see . */ -using BH.Engine.Geometry; using BH.oM.Base.Attributes; using BH.oM.Geometry; using System.Collections.Generic; @@ -39,7 +38,7 @@ public static partial class Query [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 DoesIntersect(this Polyline poly1, Polyline poly2, Plane plane = null, double tolerance = Tolerance.Distance) + 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; diff --git a/Clipper_Engine/Query/IsSame.cs b/Clipper_Engine/Query/IsSame.cs deleted file mode 100644 index 1d88b84..0000000 --- a/Clipper_Engine/Query/IsSame.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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("Checks if two polylines have the same control points. If plane is null, comparison is in 2D. If plane is not null, 3D control points are projected onto the plane before comparison.")] - [Input("pLine1", "First polyline.")] - [Input("pLine2", "Second polyline.")] - [Input("plane", "If null, comparison is in 2D (control points compared directly). If not null, 3D control points are projected onto this plane before comparison.")] - [Input("tolerance", "Distance tolerance.")] - [Output("overlaps", "True if the polylines overlap.")] - public static bool IsSame(this Polyline pLine1, Polyline pLine2, Plane plane = null, double tolerance = Tolerance.Distance) - { - if (pLine1 == null || pLine2 == null) - return false; - - if (pLine1.ControlPoints.Count != pLine2.ControlPoints.Count) - return false; - - List points1 = pLine1.ControlPoints; - List points2 = pLine2.ControlPoints; - - if (plane != null) - { - points1 = points1.Select(p => p.Project(plane)).ToList(); - points2 = points2.Select(p => p.Project(plane)).ToList(); - } - - int count = points1.Count; - - for (int i = 0; i < count; i++) - { - if (!points2[i].IsEqual(points1[0], tolerance)) - continue; - - bool matches = true; - for (int j = 0; j < count; j++) - { - if (!points1[j].IsEqual(points2[(i + j) % count], tolerance)) - { - matches = false; - break; - } - } - - if (matches) - return true; - } - - return false; - } - - /***************************************************/ - } -} From b8ac0c9b19745002ca2305bb05d5e6b8360b3a51 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Thu, 22 Oct 2026 12:33:39 +0200 Subject: [PATCH 09/14] Surface check tweaks --- Clipper_Engine/Compute/IsContaining.cs | 15 +++++++++------ Clipper_Engine/Compute/IsOnFace.cs | 23 ++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 72b2076..47624ca 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -181,16 +181,19 @@ public static bool IsContaining(this Polyline region, List refRegions, 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))) + foreach (Polyline refRegion in refRegions) { - Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); - return false; + if (refRegion.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) + { + Base.Compute.RecordError("Clipper IsContaining method only works for coplanar polylines."); + return false; + } } + } + foreach (Polyline refRegion in refRegions) + { if (!region.IsContaining(refRegion, plane, tolerance)) return false; } diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs index a67fefc..8a79d08 100644 --- a/Clipper_Engine/Compute/IsOnFace.cs +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -45,7 +45,8 @@ public static bool IsOnFace(this Point point, PlanarSurface surface, double tole if (point == null || surface?.ExternalBoundary == null) return false; - if (!TryGetPolylineBoundaries(surface, out Polyline outer, out List inner)) + (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; @@ -91,30 +92,26 @@ public static bool IsOnFace(this Point point, Polyline outer, List inn /**** Private methods ****/ /***************************************************/ - private static bool TryGetPolylineBoundaries(PlanarSurface surface, out Polyline outer, out List inner) + private static (Polyline outer, List inner) PolylineBoundaries(PlanarSurface surface) { - outer = null; - inner = new List(); - - Polyline externalPolyline = surface.ExternalBoundary as Polyline; - if (externalPolyline == null) - return false; - - outer = externalPolyline; + Polyline outer = surface.ExternalBoundary as Polyline; + if (outer == null) + return (null, null); + List inner = new List(); if (surface.InternalBoundaries == null) - return true; + return (outer, inner); foreach (ICurve boundary in surface.InternalBoundaries) { Polyline innerPolyline = boundary as Polyline; if (innerPolyline == null) - return false; + return (null, null); inner.Add(innerPolyline); } - return true; + return (outer, inner); } /***************************************************/ From 9c654559e92aa362ebed035975338703528e8f1c Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Thu, 22 Oct 2026 12:39:21 +0200 Subject: [PATCH 10/14] Clean up --- Clipper_Engine/Compute/IsContaining.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 47624ca..97db49d 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -156,9 +156,6 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane } } - if (region.IsContaining(refRegion.ControlPoints, plane, true, tolerance) == false) - return false; - //If refRegion sits fully inside the outer region, the boolean difference result should be empty return refRegion.BooleanDifference(new List { region }, plane, tolerance)?.Count == 0; } From f726f2ceaef74911ad42b4e8bbbc91005cda21eb Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Thu, 22 Oct 2026 16:56:57 +0200 Subject: [PATCH 11/14] IsClosed check added --- Clipper_Engine/Compute/BooleanDifference.cs | 6 ++++ Clipper_Engine/Compute/BooleanIntersection.cs | 6 ++++ Clipper_Engine/Compute/BooleanUnion.cs | 6 ++++ Clipper_Engine/Compute/IsContaining.cs | 30 ++++++++----------- Clipper_Engine/Compute/IsOnFace.cs | 6 ++++ 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/Clipper_Engine/Compute/BooleanDifference.cs b/Clipper_Engine/Compute/BooleanDifference.cs index ee13739..bd87da9 100644 --- a/Clipper_Engine/Compute/BooleanDifference.cs +++ b/Clipper_Engine/Compute/BooleanDifference.cs @@ -67,6 +67,12 @@ public static List BooleanDifference(this Polyline region, List { region }; } + if (!region.IsClosed(tolerance) || clipRegions.Any(x => !x.IsClosed(tolerance))) + { + 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(plane, tolerance); if (orientation == null) diff --git a/Clipper_Engine/Compute/BooleanIntersection.cs b/Clipper_Engine/Compute/BooleanIntersection.cs index 03e401b..6e9873f 100644 --- a/Clipper_Engine/Compute/BooleanIntersection.cs +++ b/Clipper_Engine/Compute/BooleanIntersection.cs @@ -48,6 +48,12 @@ public static List BooleanIntersection(this Polyline poly1, Polyline p if (poly1 == null || poly2 == null || poly1.ControlPoints.Count < 3 || poly2.ControlPoints.Count < 3) return null; + if (!poly1.IsClosed(tolerance) || !poly2.IsClosed(tolerance)) + { + Base.Compute.RecordError("Boolean Intersection works on closed regions."); + return new List(); + } + if (plane == null) { plane = poly1.FitPlane(); diff --git a/Clipper_Engine/Compute/BooleanUnion.cs b/Clipper_Engine/Compute/BooleanUnion.cs index a194f64..8e849d1 100644 --- a/Clipper_Engine/Compute/BooleanUnion.cs +++ b/Clipper_Engine/Compute/BooleanUnion.cs @@ -55,6 +55,12 @@ public static List BooleanUnion(this List polylines, Plane p if (validPolylines.Count == 1) return new List { validPolylines[0] }; + if (validPolylines.Any(x => !x.IsClosed(tolerance))) + { + Base.Compute.RecordError("Boolean Union works on closed regions."); + return new List(); + } + if (plane == null) { plane = validPolylines[0].FitPlane(); diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index 97db49d..acc9f87 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/IsContaining.cs @@ -49,6 +49,9 @@ public static bool IsContaining(this Polyline region, List points, Plane if (region == null || points == null || region.ControlPoints.Count < 3 || points.Count == 0) return false; + if (!region.IsClosed(tolerance)) + return false; + if (plane == null) { plane = region.FitPlane(); @@ -110,24 +113,6 @@ public static bool IsContaining(this Polyline outer, Point point, Plane plane = /***************************************************/ - [Description("Checks if a line is contained in a polyline on the given plane, using Clipper2 point-in-polygon on the line start, end, and midpoint.")] - [Input("outer", "Closed polyline region.")] - [Input("line", "Line 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, points on the boundary are accepted.")] - [Input("tolerance", "Distance tolerance used for orientation and scaling.")] - [Output("isContaining", "True if the line start, end, and midpoint are inside the polyline.")] - public static bool IsContaining(this Polyline outer, Line line, Plane plane = null, bool acceptOnEdge = true, double tolerance = Tolerance.Distance) - { - if (outer == null || line == null || plane == null) - return false; - - Point mid = (line.Start + line.End) * 0.5; - return outer.IsContaining(new List { line.Start, line.End, mid }, plane, 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.")] @@ -140,6 +125,9 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane if (region == null || refRegion == null || region.ControlPoints.Count < 3 || refRegion.ControlPoints.Count < 3) return false; + if (!region.IsClosed(tolerance) || !refRegion.IsClosed(tolerance)) + return false; + if (plane == null) { plane = region.FitPlane(); @@ -170,6 +158,12 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane [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 plane = null, double tolerance = Tolerance.Distance) { + if (region == null || refRegions == null || !region.IsClosed(tolerance)) + return false; + + if (refRegions.Any(x => x == null || !x.IsClosed(tolerance))) + return false; + if (plane == null) { plane = region.FitPlane(); diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Compute/IsOnFace.cs index 8a79d08..d84d8bd 100644 --- a/Clipper_Engine/Compute/IsOnFace.cs +++ b/Clipper_Engine/Compute/IsOnFace.cs @@ -72,6 +72,12 @@ public static bool IsOnFace(this Point point, Polyline outer, List inn 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); From 4569956ac7d2d932ff38c7e626dff6e5125418ba Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Thu, 22 Oct 2026 17:23:05 +0200 Subject: [PATCH 12/14] IsContaining new method added --- Clipper_Engine/Compute/IsContaining.cs | 43 ++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Compute/IsContaining.cs index acc9f87..1b840c3 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Compute/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; @@ -113,6 +114,48 @@ public static bool IsContaining(this Polyline outer, Point point, Plane plane = /***************************************************/ + [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.")] From 23e275c271f028336a8dd46ca63d187c66556aa4 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Fri, 23 Oct 2026 14:26:14 +0200 Subject: [PATCH 13/14] Methods migration --- .../{Compute => Query}/IsContaining.cs | 109 ++++++++++++++++++ Clipper_Engine/{Compute => Query}/IsOnFace.cs | 0 2 files changed, 109 insertions(+) rename Clipper_Engine/{Compute => Query}/IsContaining.cs (69%) rename Clipper_Engine/{Compute => Query}/IsOnFace.cs (100%) diff --git a/Clipper_Engine/Compute/IsContaining.cs b/Clipper_Engine/Query/IsContaining.cs similarity index 69% rename from Clipper_Engine/Compute/IsContaining.cs rename to Clipper_Engine/Query/IsContaining.cs index 1b840c3..bc6996c 100644 --- a/Clipper_Engine/Compute/IsContaining.cs +++ b/Clipper_Engine/Query/IsContaining.cs @@ -58,6 +58,9 @@ public static bool IsContaining(this Polyline region, List points, Plane plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { + // #region agent log + DebugLogNonPlanar("points", region, plane, tolerance); + // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -176,6 +179,9 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { + // #region agent log + DebugLogNonPlanar("polyline", region, plane, tolerance); + // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -212,6 +218,9 @@ public static bool IsContaining(this Polyline region, List refRegions, plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { + // #region agent log + DebugLogNonPlanar("polylineList", region, plane, tolerance); + // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -236,6 +245,106 @@ public static bool IsContaining(this Polyline region, List refRegions, } /***************************************************/ + + // #region agent log + private static int s_DebugNonPlanarLogs; + + private static void DebugLogNonPlanar(string overload, Polyline region, Plane plane, double tolerance) + { + try + { + int n = System.Threading.Interlocked.Increment(ref s_DebugNonPlanarLogs); + if (n > 20) + return; + + double maxDist = -1; + int offCount = 0; + int pointCount = region?.ControlPoints?.Count ?? 0; + double zMin = 0, zMax = 0, xSpan = 0, ySpan = 0; + if (pointCount > 0) + { + zMin = double.MaxValue; + zMax = double.MinValue; + double xMin = double.MaxValue, xMax = double.MinValue, yMin = double.MaxValue, yMax = double.MinValue; + foreach (Point pt in region.ControlPoints) + { + if (pt == null) + continue; + if (pt.X < xMin) xMin = pt.X; + if (pt.X > xMax) xMax = pt.X; + if (pt.Y < yMin) yMin = pt.Y; + if (pt.Y > yMax) yMax = pt.Y; + if (pt.Z < zMin) zMin = pt.Z; + if (pt.Z > zMax) zMax = pt.Z; + if (plane != null) + { + double d = pt.Distance(plane); + if (d > maxDist) maxDist = d; + if (d > tolerance) offCount++; + } + } + xSpan = xMax - xMin; + ySpan = yMax - yMin; + } + + Plane fitWithTol = region?.FitPlane(tolerance); + double nx = plane?.Normal?.X ?? double.NaN; + double ny = plane?.Normal?.Y ?? double.NaN; + double nz = plane?.Normal?.Z ?? double.NaN; + double nLen = plane?.Normal == null ? double.NaN : Math.Sqrt(nx * nx + ny * ny + nz * nz); + + var st = new System.Diagnostics.StackTrace(1, false); + string stack = ""; + int frames = Math.Min(8, st.FrameCount); + for (int i = 0; i < frames; i++) + { + System.Reflection.MethodBase method = st.GetFrame(i)?.GetMethod(); + if (method == null) + continue; + string typeName = method.DeclaringType != null ? method.DeclaringType.Name : "?"; + stack += typeName + "." + method.Name + ">"; + } + + int containingFrames = 0; + int idx = 0; + while ((idx = stack.IndexOf("IsContaining>", idx, StringComparison.Ordinal)) >= 0) + { + containingFrames++; + idx += 14; + } + + bool lineOverload = containingFrames >= 2; + string hypothesisId = "H1"; + if (fitWithTol == null) + hypothesisId = "H5"; + else if (double.IsNaN(nLen) || nLen < 0.5 || nLen > 1.5) + hypothesisId = "H3"; + else if (maxDist >= 0 && maxDist < 0.001) + hypothesisId = "H2"; + else if (lineOverload) + hypothesisId = "H4"; + + string D(double v) => (double.IsNaN(v) || double.IsInfinity(v)) ? "null" : v.ToString("G17", System.Globalization.CultureInfo.InvariantCulture); + string json = "{\"sessionId\":\"1bcc72\",\"runId\":\"pre-fix\",\"hypothesisId\":\"" + hypothesisId + + "\",\"location\":\"IsContaining.cs\",\"message\":\"nonplanar region\",\"data\":{\"n\":" + n + + ",\"overload\":\"" + overload + "\",\"pointCount\":" + pointCount + + ",\"offCount\":" + offCount + ",\"maxDist\":" + D(maxDist) + + ",\"tolerance\":" + D(tolerance) + ",\"fitWithTolNull\":" + (fitWithTol == null ? "true" : "false") + + ",\"lineOverload\":" + (lineOverload ? "true" : "false") + + ",\"normal\":[" + D(nx) + "," + D(ny) + "," + D(nz) + "],\"normalLen\":" + D(nLen) + + ",\"xSpan\":" + D(xSpan) + ",\"ySpan\":" + D(ySpan) + + ",\"zMin\":" + D(zMin) + ",\"zMax\":" + D(zMax) + + ",\"stack\":\"" + stack.Replace("\\", "").Replace("\"", "'") + "\"},\"timestamp\":" + + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "}"; + System.IO.File.AppendAllText(@"C:\_repos\Revit_Tagging_Tool\debug-1bcc72.log", json + "\n"); + } + catch + { + } + } + // #endregion + + /***************************************************/ } } diff --git a/Clipper_Engine/Compute/IsOnFace.cs b/Clipper_Engine/Query/IsOnFace.cs similarity index 100% rename from Clipper_Engine/Compute/IsOnFace.cs rename to Clipper_Engine/Query/IsOnFace.cs From 50affd4d0980a7b4e2016d70940ee6fb2e1bc997 Mon Sep 17 00:00:00 2001 From: wojciech buda Date: Fri, 23 Oct 2026 16:40:20 +0200 Subject: [PATCH 14/14] Cleanup --- Clipper_Engine/Query/IsContaining.cs | 109 --------------------------- 1 file changed, 109 deletions(-) diff --git a/Clipper_Engine/Query/IsContaining.cs b/Clipper_Engine/Query/IsContaining.cs index bc6996c..1b840c3 100644 --- a/Clipper_Engine/Query/IsContaining.cs +++ b/Clipper_Engine/Query/IsContaining.cs @@ -58,9 +58,6 @@ public static bool IsContaining(this Polyline region, List points, Plane plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { - // #region agent log - DebugLogNonPlanar("points", region, plane, tolerance); - // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -179,9 +176,6 @@ public static bool IsContaining(this Polyline region, Polyline refRegion, Plane plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { - // #region agent log - DebugLogNonPlanar("polyline", region, plane, tolerance); - // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -218,9 +212,6 @@ public static bool IsContaining(this Polyline region, List refRegions, plane = region.FitPlane(); if (region.ControlPoints.Any(x => !x.IsInPlane(plane, tolerance))) { - // #region agent log - DebugLogNonPlanar("polylineList", region, plane, tolerance); - // #endregion Base.Compute.RecordError("Clipper IsContaining method only works for planar polylines."); return false; } @@ -245,106 +236,6 @@ public static bool IsContaining(this Polyline region, List refRegions, } /***************************************************/ - - // #region agent log - private static int s_DebugNonPlanarLogs; - - private static void DebugLogNonPlanar(string overload, Polyline region, Plane plane, double tolerance) - { - try - { - int n = System.Threading.Interlocked.Increment(ref s_DebugNonPlanarLogs); - if (n > 20) - return; - - double maxDist = -1; - int offCount = 0; - int pointCount = region?.ControlPoints?.Count ?? 0; - double zMin = 0, zMax = 0, xSpan = 0, ySpan = 0; - if (pointCount > 0) - { - zMin = double.MaxValue; - zMax = double.MinValue; - double xMin = double.MaxValue, xMax = double.MinValue, yMin = double.MaxValue, yMax = double.MinValue; - foreach (Point pt in region.ControlPoints) - { - if (pt == null) - continue; - if (pt.X < xMin) xMin = pt.X; - if (pt.X > xMax) xMax = pt.X; - if (pt.Y < yMin) yMin = pt.Y; - if (pt.Y > yMax) yMax = pt.Y; - if (pt.Z < zMin) zMin = pt.Z; - if (pt.Z > zMax) zMax = pt.Z; - if (plane != null) - { - double d = pt.Distance(plane); - if (d > maxDist) maxDist = d; - if (d > tolerance) offCount++; - } - } - xSpan = xMax - xMin; - ySpan = yMax - yMin; - } - - Plane fitWithTol = region?.FitPlane(tolerance); - double nx = plane?.Normal?.X ?? double.NaN; - double ny = plane?.Normal?.Y ?? double.NaN; - double nz = plane?.Normal?.Z ?? double.NaN; - double nLen = plane?.Normal == null ? double.NaN : Math.Sqrt(nx * nx + ny * ny + nz * nz); - - var st = new System.Diagnostics.StackTrace(1, false); - string stack = ""; - int frames = Math.Min(8, st.FrameCount); - for (int i = 0; i < frames; i++) - { - System.Reflection.MethodBase method = st.GetFrame(i)?.GetMethod(); - if (method == null) - continue; - string typeName = method.DeclaringType != null ? method.DeclaringType.Name : "?"; - stack += typeName + "." + method.Name + ">"; - } - - int containingFrames = 0; - int idx = 0; - while ((idx = stack.IndexOf("IsContaining>", idx, StringComparison.Ordinal)) >= 0) - { - containingFrames++; - idx += 14; - } - - bool lineOverload = containingFrames >= 2; - string hypothesisId = "H1"; - if (fitWithTol == null) - hypothesisId = "H5"; - else if (double.IsNaN(nLen) || nLen < 0.5 || nLen > 1.5) - hypothesisId = "H3"; - else if (maxDist >= 0 && maxDist < 0.001) - hypothesisId = "H2"; - else if (lineOverload) - hypothesisId = "H4"; - - string D(double v) => (double.IsNaN(v) || double.IsInfinity(v)) ? "null" : v.ToString("G17", System.Globalization.CultureInfo.InvariantCulture); - string json = "{\"sessionId\":\"1bcc72\",\"runId\":\"pre-fix\",\"hypothesisId\":\"" + hypothesisId - + "\",\"location\":\"IsContaining.cs\",\"message\":\"nonplanar region\",\"data\":{\"n\":" + n - + ",\"overload\":\"" + overload + "\",\"pointCount\":" + pointCount - + ",\"offCount\":" + offCount + ",\"maxDist\":" + D(maxDist) - + ",\"tolerance\":" + D(tolerance) + ",\"fitWithTolNull\":" + (fitWithTol == null ? "true" : "false") - + ",\"lineOverload\":" + (lineOverload ? "true" : "false") - + ",\"normal\":[" + D(nx) + "," + D(ny) + "," + D(nz) + "],\"normalLen\":" + D(nLen) - + ",\"xSpan\":" + D(xSpan) + ",\"ySpan\":" + D(ySpan) - + ",\"zMin\":" + D(zMin) + ",\"zMax\":" + D(zMax) - + ",\"stack\":\"" + stack.Replace("\\", "").Replace("\"", "'") + "\"},\"timestamp\":" - + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "}"; - System.IO.File.AppendAllText(@"C:\_repos\Revit_Tagging_Tool\debug-1bcc72.log", json + "\n"); - } - catch - { - } - } - // #endregion - - /***************************************************/ } }