first commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System.Collections.Generic;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Generates clipped shapes from one or more input paths using polygon boolean operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class provides a high-level wrapper around the low-level <see cref="PolygonClipperAction"/>.
|
||||
/// It accumulates subject and clip polygons, applies the specified <see cref="BooleanOperation"/>,
|
||||
/// and converts the resulting polygon contours back into <see cref="ComplexPolygon"/> instances suitable
|
||||
/// for rendering or further processing.
|
||||
/// </remarks>
|
||||
internal static class ClippedShapeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the final clipped shapes from the previously provided subject and clip paths.
|
||||
/// </summary>
|
||||
/// <param name="operation">
|
||||
/// The boolean operation to perform, such as <see cref="BooleanOperation.Union"/>,
|
||||
/// <see cref="BooleanOperation.Intersection"/>, or <see cref="BooleanOperation.Difference"/>.
|
||||
/// </param>
|
||||
/// <param name="subject">The subject path.</param>
|
||||
/// <param name="clip">The clipping paths.</param>
|
||||
/// <returns>
|
||||
/// The <see cref="ComplexPolygon"/> representing the result of the boolean operation.
|
||||
/// </returns>
|
||||
public static ComplexPolygon GenerateClippedShapes(
|
||||
BooleanOperation operation,
|
||||
IPath subject,
|
||||
IEnumerable<IPath> clip)
|
||||
{
|
||||
Guard.NotNull(subject);
|
||||
Guard.NotNull(clip);
|
||||
|
||||
PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten());
|
||||
PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip);
|
||||
|
||||
PCPolygon result = operation switch
|
||||
{
|
||||
BooleanOperation.Xor => PolygonClipperAction.Xor(s, c),
|
||||
BooleanOperation.Difference => PolygonClipperAction.Difference(s, c),
|
||||
BooleanOperation.Union => PolygonClipperAction.Union(s, c),
|
||||
_ => PolygonClipperAction.Intersection(s, c),
|
||||
};
|
||||
|
||||
IPath[] shapes = new IPath[result.Count];
|
||||
|
||||
int index = 0;
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
shapes[index++] = new Polygon(CreateContourPoints(result, i));
|
||||
}
|
||||
|
||||
return new(shapes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PolygonClipper contour to ImageSharp points and normalizes winding for parent/child rings.
|
||||
/// </summary>
|
||||
/// <param name="polygon">The polygon containing the contour hierarchy.</param>
|
||||
/// <param name="contourIndex">The contour index to convert.</param>
|
||||
/// <returns>The converted point array.</returns>
|
||||
private static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex)
|
||||
{
|
||||
Contour contour = polygon[contourIndex];
|
||||
PointF[] points = new PointF[contour.Count];
|
||||
bool reverse = ShouldReverseForNonZeroWinding(polygon, contourIndex);
|
||||
|
||||
if (!reverse)
|
||||
{
|
||||
for (int i = 0; i < contour.Count; i++)
|
||||
{
|
||||
Vertex vertex = contour[i];
|
||||
points[i] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
for (int sourceIndex = contour.Count - 1, targetIndex = 0; sourceIndex >= 0; sourceIndex--, targetIndex++)
|
||||
{
|
||||
Vertex vertex = contour[sourceIndex];
|
||||
points[targetIndex] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures child contours (holes/islands) use opposite winding to their direct parent.
|
||||
/// This keeps clipped output deterministic when consumed with the NonZero fill rule.
|
||||
/// </summary>
|
||||
/// <param name="polygon">The polygon containing contour hierarchy information.</param>
|
||||
/// <param name="contourIndex">The contour index to inspect.</param>
|
||||
/// <returns><see langword="true"/> when the contour should be reversed.</returns>
|
||||
private static bool ShouldReverseForNonZeroWinding(PCPolygon polygon, int contourIndex)
|
||||
{
|
||||
Contour contour = polygon[contourIndex];
|
||||
if (contour.ParentIndex is not int parentIndex || (uint)parentIndex >= (uint)polygon.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Contour parentContour = polygon[parentIndex];
|
||||
return contour.IsCounterClockwise() == parentContour.IsCounterClockwise();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Builders for <see cref="PCPolygon"/> from ImageSharp paths.
|
||||
/// Converts ImageSharp paths to the format required by PolygonClipper.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PolygonClipper computes parent-child relationships, depth, and orientation during its
|
||||
/// sweep line algorithm, so we only need to provide contours with vertices.
|
||||
/// </remarks>
|
||||
internal static class PolygonClipperFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a polygon from multiple paths.
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths to convert.</param>
|
||||
/// <returns>A <see cref="PCPolygon"/> containing all flattened paths as contours.</returns>
|
||||
public static PCPolygon FromClosedPaths(IEnumerable<IPath> paths)
|
||||
{
|
||||
PCPolygon polygon = [];
|
||||
|
||||
foreach (IPath path in paths)
|
||||
{
|
||||
polygon = FromSimpleClosedPaths(path.Flatten(), polygon);
|
||||
}
|
||||
|
||||
return polygon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts closed simple paths to PolygonClipper contours.
|
||||
/// </summary>
|
||||
/// <param name="paths">Closed simple paths.</param>
|
||||
/// <param name="polygon">Optional existing polygon to populate.</param>
|
||||
/// <returns>The constructed <see cref="PCPolygon"/>.</returns>
|
||||
/// <remarks>
|
||||
/// This method simply converts ImageSharp paths to PolygonClipper contours by copying vertices.
|
||||
/// PolygonClipper's sweep line algorithm will determine parent-child relationships, depth,
|
||||
/// and proper orientation during clipping operations. We only need to ensure paths are
|
||||
/// closed and have sufficient vertices.
|
||||
/// </remarks>
|
||||
public static PCPolygon FromSimpleClosedPaths(IEnumerable<ISimplePath> paths, PCPolygon? polygon = null)
|
||||
{
|
||||
polygon ??= [];
|
||||
|
||||
foreach (ISimplePath p in paths)
|
||||
{
|
||||
if (!p.IsClosed)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ReadOnlySpan<PointF> points = p.Points.Span;
|
||||
if (points.Length < 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Contour contour = [];
|
||||
|
||||
// Copy all vertices
|
||||
for (int i = 0; i < points.Length; i++)
|
||||
{
|
||||
contour.Add(new Vertex(points[i].X, points[i].Y));
|
||||
}
|
||||
|
||||
// Add the contour - PolygonClipper will determine parent/depth/orientation during sweep
|
||||
polygon.Add(contour);
|
||||
}
|
||||
|
||||
return polygon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
||||
|
||||
using SixLabors.PolygonClipper;
|
||||
using System;
|
||||
using PCPolygon = SixLabors.PolygonClipper.Polygon;
|
||||
using StrokeOptions = SixLabors.ImageSharp.Drawing.Processing.StrokeOptions;
|
||||
|
||||
namespace SixLabors.ImageSharp.Drawing.PolygonGeometry {
|
||||
/// <summary>
|
||||
/// Generates stroked and merged shapes using polygon stroking and boolean clipping.
|
||||
/// </summary>
|
||||
internal static class StrokedShapeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Strokes a path and returns a merged outline from its flattened segments.
|
||||
/// </summary>
|
||||
/// <param name="path">The source path. It is flattened using the current flattening settings.</param>
|
||||
/// <param name="width">The stroke width in the caller's coordinate space.</param>
|
||||
/// <param name="options">The stroke geometry options.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="ComplexPolygon"/> representing the stroked outline after boolean merge.
|
||||
/// </returns>
|
||||
public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options)
|
||||
{
|
||||
// 1) Stroke the input path as open or closed.
|
||||
PCPolygon rings = [];
|
||||
|
||||
foreach (ISimplePath sp in path.Flatten())
|
||||
{
|
||||
ReadOnlySpan<PointF> span = sp.Points.Span;
|
||||
|
||||
if (span.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Contour ring = new(span.Length);
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
PointF p = span[i];
|
||||
ring.Add(new Vertex(p.X, p.Y));
|
||||
}
|
||||
|
||||
if (sp.IsClosed)
|
||||
{
|
||||
ring.Add(ring[0]);
|
||||
}
|
||||
|
||||
rings.Add(ring);
|
||||
}
|
||||
|
||||
int count = rings.Count;
|
||||
if (count == 0)
|
||||
{
|
||||
return new([]);
|
||||
}
|
||||
|
||||
PCPolygon result = PolygonStroker.Stroke(rings, width, CreateStrokeOptions(options));
|
||||
|
||||
IPath[] shapes = new IPath[result.Count];
|
||||
int index = 0;
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Contour contour = result[i];
|
||||
PointF[] points = new PointF[contour.Count];
|
||||
|
||||
for (int j = 0; j < contour.Count; j++)
|
||||
{
|
||||
Vertex vertex = contour[j];
|
||||
points[j] = new PointF((float)vertex.X, (float)vertex.Y);
|
||||
}
|
||||
|
||||
shapes[index++] = new Polygon(points);
|
||||
}
|
||||
|
||||
return new(shapes);
|
||||
}
|
||||
|
||||
private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options)
|
||||
{
|
||||
PolygonClipper.StrokeOptions o = new()
|
||||
{
|
||||
ArcDetailScale = options.ArcDetailScale,
|
||||
MiterLimit = options.MiterLimit,
|
||||
LineJoin = options.LineJoin switch
|
||||
{
|
||||
LineJoin.MiterRound => PolygonClipper.LineJoin.MiterRound,
|
||||
LineJoin.Bevel => PolygonClipper.LineJoin.Bevel,
|
||||
LineJoin.Round => PolygonClipper.LineJoin.Round,
|
||||
LineJoin.MiterRevert => PolygonClipper.LineJoin.MiterRevert,
|
||||
_ => PolygonClipper.LineJoin.Miter,
|
||||
},
|
||||
|
||||
LineCap = options.LineCap switch
|
||||
{
|
||||
LineCap.Round => PolygonClipper.LineCap.Round,
|
||||
LineCap.Square => PolygonClipper.LineCap.Square,
|
||||
_ => PolygonClipper.LineCap.Butt,
|
||||
}
|
||||
};
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user