/// <summary> /// Generates a trapezoid polygon on the XY plane with the midpoint of each edge /// centred on the origin and with a different top and bottom width. /// </summary> /// <param name="depth">The depth of the trapezoid</param> /// <param name="topWidth">The width of the top of the trapezoid</param> /// <param name="baseWidth">The width of the base of the trapezoid</param> /// <returns></returns> public static PolyCurve Trapezoid(double depth, double topWidth, double baseWidth) { double xT = topWidth / 2; double xB = baseWidth / 2; double y = depth / 2; PolyCurve result = new PolyCurve(new Line(xT, y, -xT, y)); result.AddLine(-xB, -y); result.AddLine(xB, -y); result.AddLine(xT, y); return(result); }
/// <summary> /// Extract a portion of this curve as a new curve /// </summary> /// <param name="subDomain">The subdomain of this curve to /// be extracted as a new curve</param> /// <returns></returns> public override Curve Extract(Interval subDomain) { var result = new PolyCurve(); Interval subDomainAdj = subDomain; if (subDomain.IsDecreasing) { subDomainAdj = new Interval(subDomain.Start, 1.0); } PopulateWithSubCurves(subDomainAdj, result); // If looping, do a second pass: if (Closed && subDomain.IsDecreasing) { PopulateWithSubCurves(new Maths.Interval(0.0, subDomain.End), result); } return(result); }
/// <summary> /// Get a closed polycurve containing the edges of this section of the path. /// The edges of the path must have been generated first. /// </summary> /// <returns></returns> public static PolyCurve GetBoundaryCurve <TPath>(this TPath path) where TPath : IWidePath { var result = new PolyCurve(); if (path.StartCapLeft != null) { result.Add(path.StartCapLeft); } if (path.LeftEdge != null) { result.Add(path.LeftEdge, true); } if (path.EndCapLeft != null) { result.Add(path.EndCapLeft, true); } if (path.EndCapRight != null) { result.Add(path.EndCapRight.Reversed(), true); } if (path.RightEdge != null) { result.Add(path.RightEdge.Reversed(), true); } if (path.StartCapRight != null) { result.Add(path.StartCapRight.Reversed(), true); } result.Close(); return(result); }
/// <summary> /// Split this region into two (or more) sub-regions along a straight line /// </summary> /// <param name="splitPt">A point on the splitting line</param> /// <param name="splitDir">The direction of the line</param> /// <param name="splitWidth">Optional. The width of the split.</param> /// <returns>The resultant list of regions. If the line does not bisect /// this region and the region could not be split, this collection will contain /// only the original region.</returns> public IList <PlanarRegion> SplitByLineXY(Vector splitPt, Vector splitDir, double splitWidth = 0) { var result = new List <PlanarRegion>(); var lineInts = new List <double>(); var outerInts = Intersect.CurveLineXY(Perimeter, splitPt, splitDir, null, 0, 1, false, lineInts); if (outerInts.Count > 1) { // Sort intersections by position along curve: var sortedInts = new SortedList <double, double>(outerInts.Count); for (int i = 0; i < outerInts.Count; i++) { sortedInts.Add(outerInts[i], lineInts[i]); } outerInts = sortedInts.Keys.ToList(); lineInts = sortedInts.Values.ToList(); int offset = lineInts.IndexOfMin(); outerInts.Shift(offset); lineInts.Shift(offset); // Create segments data structure var segments = new List <PerimeterSegment>(outerInts.Count - 1); for (int i = 0; i < outerInts.Count; i++) { double t0 = outerInts[i]; double t1 = outerInts.GetWrapped(i + 1); double tC0 = lineInts[i]; double tC1 = lineInts.GetWrapped(i + 1); var segment = new PerimeterSegment(Perimeter, t0, t1, tC0, tC1); segments.Add(segment); } //TODO: void intersections bool backwards = true; while (segments.Count > 0) { var offsets = new List <double>(); PerimeterSegment segment = segments.First(); PolyCurve newPerimeter = segment.Extract().ToPolyCurve(); for (int i = 0; i < newPerimeter.SegmentCount; i++) { offsets.Add(0); } PerimeterSegment nextSegment = FindNextPerimeterSegment(segments, segment.CutterDomain.End, backwards); while (nextSegment != null && nextSegment != segment) { Curve nextCurve = nextSegment.Extract(); newPerimeter.AddLine(nextCurve.StartPoint); offsets.Add(splitWidth / 2); newPerimeter.Add(nextCurve); for (int i = 0; i < nextCurve.SegmentCount; i++) { offsets.Add(0); } segments.Remove(nextSegment); nextSegment = FindNextPerimeterSegment(segments, nextSegment.CutterDomain.End, backwards); } segments.RemoveAt(0); if (!newPerimeter.Closed) { /*if (splitWidth > 0) * { * // Temporary bodge to get rid of 'blades' at ends of split * Vector endToEnd = (newPerimeter.StartPoint - newPerimeter.EndPoint).Unitize(); * var line = new Line( * newPerimeter.EndPoint - endToEnd * splitWidth / 4, * newPerimeter.StartPoint + endToEnd * splitWidth / 4); * newPerimeter.AddLine(line.StartPoint); * offsets.Add(0); * newPerimeter.Add(line); * offsets.Add(splitWidth / 2); * newPerimeter.Close(); * offsets.Add(0); * } * else * {*/ newPerimeter.Close(); offsets.Add(splitWidth / 2); //} } backwards = !backwards; if (splitWidth > 0) { var newNewPerimeter = newPerimeter.OffsetInwards(offsets); // Check offset has not inverted perimeter: // TODO: Do this automatically when offsetting? if (newNewPerimeter != null && newNewPerimeter.IsClockwiseXY() == newPerimeter.IsClockwiseXY()) { newPerimeter = newNewPerimeter.ToPolyCurve(); } else { newPerimeter = null; } } if (newPerimeter != null) { result.Add(new PlanarRegion(newPerimeter, Attributes?.Duplicate())); } } // OLD VERSION: /*for (int i = 0; i < outerInts.Count; i++) * { * double t0 = outerInts[i]; * double t1 = outerInts.GetWrapped(i + 1); * Curve newPerimeter = Perimeter.Extract(new Interval(t0, t1))?.ToPolyCurve(); * //TODO: Cut through and include voids * if (!newPerimeter.Closed) * { * ((PolyCurve)newPerimeter).Close(); * if (splitWidth > 0) * { * var offsets = new double[newPerimeter.SegmentCount]; * offsets[offsets.Length - 1] = splitWidth / 2; * var newNewPerimeter = newPerimeter.OffsetInwards(offsets); * // Check offset has not inverted perimeter: * // TODO: Do this automatically when offsetting? * if (newNewPerimeter != null && newNewPerimeter.IsClockwiseXY() == newPerimeter.IsClockwiseXY()) * { * newPerimeter = newNewPerimeter; * } * else newPerimeter = null; * } * } * if (newPerimeter != null) result.Add(new PlanarRegion(newPerimeter, Attributes?.Duplicate())); * }*/ } else { result.Add(this); //Return the original } return(result); }
/// <summary> /// Split this region into two (or more) sub-regions along a straight line /// </summary> /// <param name="splitPt">A point on the splitting line</param> /// <param name="splitDir">The direction of the line</param> /// <param name="splitWidth">Optional. The width of the split.</param> /// <returns>The resultant list of regions. If the line does not bisect /// this region and the region could not be split, this collection will contain /// only the original region.</returns> public IList <PlanarRegion> SplitByLineXY2(Vector splitPt, Vector splitDir, double splitWidth = 0) { var result = new List <PlanarRegion>(); // Offset splitting line by half splitWidth in each direction: Vector offsetDir = splitDir.PerpendicularXY().Unitize(); Vector offset = offsetDir * splitWidth / 2; Vector leftPt = splitPt + offset; Vector rightPt = splitPt - offset; // Find intersection points on perimeter: var perimeterInts = new List <CurveLineIntersection>(); // Left side: IList <CurveLineIntersection> leftInts = Intersect.CurveLineXYIntersections(Perimeter, leftPt, splitDir); perimeterInts.AddRange(leftInts); IList <CurveLineIntersection> rightInts; // Shortcut: If the split has no width, left and right intersections will be the same if (splitWidth == 0) { rightInts = leftInts; } else { rightInts = Intersect.CurveLineXYIntersections(Perimeter, rightPt, splitDir); perimeterInts.AddRange(rightInts); } // If no intersections on the perimeter, the region does not need to be split: if (leftInts.Count <= 1 && rightInts.Count <= 1) { result.Add(this); return(result); } // Voids which do not intersect but will need to be placed: CurveCollection freeVoids = new CurveCollection(); // Find intersection points on voids: foreach (var voidCrv in Voids) { // Left side: var leftVoidInts = Intersect.CurveLineXYIntersections(voidCrv, leftPt, splitDir); foreach (var vI in leftVoidInts) { leftInts.Add(vI); } IList <CurveLineIntersection> rightVoidInts; // Shortcut: If the split has no width, left and right intersections will be the same if (splitWidth == 0) { rightVoidInts = leftVoidInts; } else { rightVoidInts = Intersect.CurveLineXYIntersections(voidCrv, rightPt, splitDir); } foreach (var vI in rightVoidInts) { rightInts.Add(vI); } if (leftVoidInts.Count <= 1 && rightVoidInts.Count <= 1) { // Void is wholly to one side or the other... freeVoids.Add(voidCrv); } } // Tag left and right intersections foreach (var lI in leftInts) { lI.Side = HandSide.Left; } foreach (var rI in rightInts) { rI.Side = HandSide.Right; } // Determine direction of travel along cutting lines: int rightDir = RightCutterDirectionOfTravel(perimeterInts); // 'Fake' intersection to represent perimeter start: CurveLineIntersection startInt = new CurveLineIntersection(Perimeter, 0, double.NaN); // Loop through intersections and build up new sub-regions by traversing connected // intersections along the perimeter, cutting lines and void curves CurveLineIntersection currentInt = startInt; CurveLineIntersection nextStartInt = null; PolyCurve newPerimeter = null; PlanarRegion newRegion = null; // The cutter to travel along (Undefined is perimeter or void cuve): HandSide travelSide = HandSide.Undefined; int segmentCount = perimeterInts.Count + 1; //TODO: Include void intersections for (int i = 0; i < segmentCount; i++) // Loop through intersections { CurveLineIntersection nextInt; // Travel to next intersection, along the cutters or perimeter if (travelSide == HandSide.Left) { // Travel along left cutter if (rightDir == 1) { nextInt = leftInts.ItemWithPrevious(cLI => cLI.LineParameter, currentInt.LineParameter); } else { nextInt = leftInts.ItemWithNext(cLI => cLI.LineParameter, currentInt.LineParameter); } } else if (travelSide == HandSide.Right) { // Travel along right cutter if (rightDir == 1) { nextInt = rightInts.ItemWithNext(cLI => cLI.LineParameter, currentInt.LineParameter); } else { nextInt = rightInts.ItemWithPrevious(cLI => cLI.LineParameter, currentInt.LineParameter); } } else { // Travel to next intersection on perimeter nextInt = perimeterInts.ItemWithNext(cLI => cLI.CurveParameter, currentInt.CurveParameter); // 'Fake' intersection to represent perimeter end: if (nextInt == null) { nextInt = new CurveLineIntersection(Perimeter, 1, double.NaN); } if (nextStartInt == null) { nextStartInt = nextInt; } } if (nextInt != null && currentInt.Curve == nextInt.Curve && // Check both ints on same curve (travelSide == HandSide.Undefined || currentInt.Curve != Perimeter)) { Curve curve = nextInt.Curve; Interval subDomain = new Interval(currentInt.CurveParameter, nextInt.CurveParameter); Vector crvPt = curve.PointAt(subDomain.Mid); HandSide side = SideOfSplit(crvPt, leftPt, rightPt, offsetDir); if (side != HandSide.Undefined) { // TODO: Adjust void curve direction // Add perimeter to outputs Curve subCrv = curve.Extract(subDomain); if (newPerimeter == null) { newPerimeter = new PolyCurve(subCrv.Explode()); newRegion = new PlanarRegion(newPerimeter); result.Add(newRegion); } else { newPerimeter.Add(subCrv, true, true); } } else { //Traversing the gap, need a new start Point startInt = nextInt; } } // Next side to travel: if (travelSide != HandSide.Undefined && (nextInt == null || nextInt.Curve == Perimeter)) { travelSide = HandSide.Undefined; } else { travelSide = nextInt.Side; } if (nextInt == null || nextInt == startInt || nextInt.CurveParameter >= 1) { // Reset to start next segment nextInt = nextStartInt; startInt = nextStartInt; nextStartInt = null; if (newPerimeter != null) { newPerimeter.Close(); } newPerimeter = null; newRegion = null; travelSide = HandSide.Undefined; } currentInt = nextInt; } if (newPerimeter != null) { newPerimeter.Close(); } //TODO: Assign un-intersected voids return(result); }
/// <summary> /// Offset this curve on the XY plane by varying distances for /// each span. /// </summary> /// <param name="distances">The offset distance. /// Positive numbers will result in the offset curve being to the right-hand /// side, looking along the curve. Negative numbers to the left.</param> /// <param name="tidy">If true (default) collapsed segments will be removed.</param> /// <returns></returns> public override Curve Offset(IList <double> distances, bool tidy = true, bool copyAttributes = true) { //TODO: Implement collapsed segments tidying var result = new PolyCurve(); int distIndex = 0; // Offset sub curves: foreach (Curve crv in SubCurves) { Curve offsetCrv = crv.Offset(distances.SubListFrom(distIndex)); distIndex += crv.SegmentCount; if (distIndex > distances.Count - 1) { distIndex = distances.Count - 1; } if (offsetCrv != null) { if (result.SubCurves.Count > 0) { // Adjust offset curve ends to node out Curve prevCrv = result.SubCurves.Last(); MatchEnds(prevCrv.End, offsetCrv.Start); } result.Add(offsetCrv); if (copyAttributes && offsetCrv.Attributes == null) { offsetCrv.Attributes = Attributes; } } } // Match end to start if (Closed && result.SubCurves.Count > 1) { MatchEnds(result.SubCurves.Last().End, result.SubCurves.First().Start); } if (tidy) { List <Curve> originals = new List <Curve>(); originals.AddRange(SubCurves); // Removed flipped segments int j = 0; while (j < result.SubCurves.Count) { Curve offset = result.SubCurves[j]; Curve original = originals[j]; if (IsOffsetCurveFlipped(original, offset)) { if (result.SubCurves.Count > 0) { Curve previous = result.SubCurves.GetWrapped(j - 1); Curve subsequent = result.SubCurves.GetWrapped(j + 1); bool worked = MatchEnds(previous.End, subsequent.Start); //TODO: Deal with failed matches (for e.g. when adjacent edges are parallel) //if (!worked) //return null; //TEMP: prevents invalid offsets, but a bit overkilly! } result.SubCurves.RemoveAt(j); originals.RemoveAt(j); //j--; } else { j++; } } if (!result.IsValid) { return(null); } } return(result); }
/// <summary> /// Creates a new polycurve as a copy of another one /// </summary> /// <param name="other"></param> public PolyCurve(PolyCurve other) : this(other.FastDuplicateSubCurves(), other.Attributes) { }
/// <summary> /// Extract from this PolyCurve a chain of subcurves, starting with the /// longest and continuing to select the longest edge until no more can /// be added without adding an edge which is within an angle tolerance of /// an existing curve within the chain. /// </summary> /// <returns></returns> public PolyCurve ExtractLongCurveChain(Angle maxAngle) { Curve longest = SubCurves.GetLongest(); if (longest != null) { var tangents = new List <Vector>(); tangents.Add(longest.TangentAt(0.5)); var result = new PolyCurve(longest); int iL = SubCurves.IndexOf(longest); int i0 = iL - 1; int i1 = iL + 1; bool closed = Closed; bool tryPre = closed || i0 >= 0; bool tryPost = closed || i1 < SubCurves.Count; while (tryPre || tryPost) { Curve pre = tryPre ? SubCurves.GetWrapped(i0, closed) : null; Curve post = tryPost ? SubCurves.GetWrapped(i1, closed) : null; if (post != null && (pre == null || post.Length > pre.Length)) { Vector tang = post.TangentAt(0.5); if (result.SubCurves.Contains(post.GUID) || tangents.MaximumAngleBetween(tang).Abs() > maxAngle) { tryPost = false; } else { result.SubCurves.Add(post); tangents.Add(tang); i1++; if (!closed && i1 >= SubCurves.Count) { tryPost = false; } } } else if (pre != null) { Vector tang = pre.TangentAt(0.5); if (result.SubCurves.Contains(pre.GUID) || tangents.MaximumAngleBetween(tang).Abs() > maxAngle) { tryPre = false; } else { result.SubCurves.Insert(0, pre); tangents.Add(tang); i0--; if (!closed && i0 < 0) { tryPre = false; } } } else { tryPre = false; tryPost = false; } } return(result); } return(null); }