first commit

This commit is contained in:
2026-08-03 22:31:27 +02:00
commit 7e8cddf208
1986 changed files with 370602 additions and 0 deletions
@@ -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;
}
}
}