znlgis 博客

GIS开发与技术分享

第16章:ClipperOffset 偏移类详解

16.1 概述

ClipperOffset 类用于对多边形和路径进行偏移(膨胀或收缩)操作。这在 CAD、CNC 加工、地图缓冲区分析等领域有广泛应用。

16.2 类定义

16.2.1 类声明

public class ClipperOffset
{
    // 选项
    private double _miterLimit;
    private double _arcTolerance;
    private bool _preserveCollinear;
    private bool _reverseSolution;
    
    // 内部数据
    private readonly List<Group> _groupList;
    private readonly PathD _normals;
    private readonly Paths64 _solution;
    
    // delta 相关
    private double _groupDelta;
    private double _delta;
    private double _absGroupDelta;
    private double _mitLimSqr;
    private double _stepsPerRad;
    private double _stepSin;
    private double _stepCos;
    
    // 构造函数
    public ClipperOffset(double miterLimit = 2.0, 
        double arcTolerance = 0.25,
        bool preserveCollinear = false,
        bool reverseSolution = false)
    {
        _miterLimit = miterLimit;
        _arcTolerance = arcTolerance;
        _preserveCollinear = preserveCollinear;
        _reverseSolution = reverseSolution;
        _groupList = new List<Group>();
        _normals = new PathD();
        _solution = new Paths64();
    }
}

16.2.2 Group 类

internal class Group
{
    internal Paths64 paths;
    internal JoinType joinType;
    internal EndType endType;
    internal bool pathsReversed;
    
    public Group(Paths64 paths, JoinType joinType, EndType endType)
    {
        this.paths = paths;
        this.joinType = joinType;
        this.endType = endType;
    }
}

16.3 JoinType 连接类型

16.3.1 枚举定义

public enum JoinType
{
    Miter,    // 斜接
    Square,   // 方形
    Bevel,    // 斜角
    Round     // 圆角
}

16.3.2 Miter(斜接)

原始:           Miter 偏移:
   ╲                 ╱│
    ╲               ╱ │
     ╲             ╱  │
      ╲──────────╱    │
                      │
                      │

延伸两条偏移边到相交点
受 MiterLimit 限制

16.3.3 Square(方形)

原始:           Square 偏移:
   ╲                 ┌─┐
    ╲               ╱  │
     ╲             ╱   │
      ╲──────────╱─────┘

在拐角处添加方形

16.3.4 Bevel(斜角)

原始:           Bevel 偏移:
   ╲                  ╱
    ╲                ╱│
     ╲              ╱ │
      ╲──────────╱───┘

直接连接两条偏移边

16.3.5 Round(圆角)

原始:           Round 偏移:
   ╲                 ╭─╮
    ╲               ╱   ╲
     ╲             ╱     │
      ╲──────────╱──────╯

使用圆弧连接

16.4 EndType 端点类型

16.4.1 枚举定义

public enum EndType
{
    Polygon,   // 闭合多边形
    Joined,    // 连接的开放路径
    Butt,      // 平头
    Square,    // 方形端
    Round      // 圆形端
}

16.4.2 各类型图示

原始路径:○────────────────○

Polygon (闭合):
┌──────────────────┐
│                  │
│  ○────────────○  │
│                  │
└──────────────────┘

Joined (连接):
○────────────────○
│                │
○────────────────○

Butt (平头):
│────────────────│
│                │
│────────────────│

Square (方形端):
┌────────────────┐
│                │
│                │
│                │
└────────────────┘

Round (圆形端):
╭────────────────╮
│                │
│                │
│                │
╰────────────────╯

16.5 AddPath/AddPaths 方法

16.5.1 添加路径

public void AddPath(Path64 path, JoinType joinType, EndType endType)
{
    if (path.Count == 0) return;
    
    Paths64 paths = new Paths64(1) { path };
    AddPaths(paths, joinType, endType);
}

public void AddPaths(Paths64 paths, JoinType joinType, EndType endType)
{
    if (paths.Count == 0) return;
    
    _groupList.Add(new Group(paths, joinType, endType));
}

16.5.2 清除数据

public void Clear()
{
    _groupList.Clear();
}

16.6 Execute 方法

16.6.1 执行偏移

public void Execute(double delta, Paths64 solution)
{
    solution.Clear();
    
    if (_groupList.Count == 0) return;
    
    _solution.Clear();
    
    // 设置偏移量
    _delta = delta;
    
    // 处理每个组
    foreach (Group group in _groupList)
    {
        DoGroupOffset(group);
    }
    
    // 合并结果
    if (_solution.Count > 0)
    {
        // 使用裁剪器合并可能重叠的区域
        Clipper64 clipper = new Clipper64();
        clipper.PreserveCollinear = _preserveCollinear;
        clipper.ReverseSolution = _reverseSolution;
        clipper.AddSubject(_solution);
        clipper.Execute(ClipType.Union, FillRule.Positive, solution);
    }
}

16.6.2 Execute 重载

// 输出到 PolyTree64
public void Execute(double delta, PolyTree64 polytree)
{
    Paths64 paths = new Paths64();
    Execute(delta, paths);
    
    // 构建 PolyTree
    BuildPolyTree(paths, polytree);
}

// 浮点版本
public void Execute(double delta, PathsD solution)
{
    Paths64 paths64 = new Paths64();
    Execute(delta, paths64);
    
    // 转换为浮点
    solution.Clear();
    foreach (Path64 path in paths64)
    {
        PathD pathD = new PathD(path.Count);
        foreach (Point64 pt in path)
        {
            pathD.Add(new PointD(pt));
        }
        solution.Add(pathD);
    }
}

16.7 DoGroupOffset

16.7.1 组偏移处理

private void DoGroupOffset(Group group)
{
    if (group.endType == EndType.Polygon)
    {
        // 闭合多边形
        _groupDelta = group.pathsReversed ? -_delta : _delta;
    }
    else
    {
        // 开放路径
        _groupDelta = Math.Abs(_delta);
    }
    
    _absGroupDelta = Math.Abs(_groupDelta);
    
    // 计算斜接限制
    if (_miterLimit > 1)
        _mitLimSqr = 2 / (_miterLimit * _miterLimit);
    else
        _mitLimSqr = 2;
    
    // 计算圆弧步进
    if (_absGroupDelta > 0)
    {
        double arcTol = _arcTolerance > 0 ? 
            _arcTolerance : 
            _absGroupDelta * 0.25;
        
        _stepsPerRad = Math.PI / Math.Acos(1 - arcTol / _absGroupDelta);
        _stepSin = Math.Sin(2 * Math.PI / _stepsPerRad);
        _stepCos = Math.Cos(2 * Math.PI / _stepsPerRad);
        
        if (_groupDelta < 0) _stepSin = -_stepSin;
    }
    
    // 处理每条路径
    foreach (Path64 path in group.paths)
    {
        DoPath(path, group);
    }
}

16.8 DoPath 路径偏移

16.8.1 处理单条路径

private void DoPath(Path64 path, Group group)
{
    int pathLen = path.Count;
    if (pathLen < 2) return;
    
    // 构建法线向量
    BuildNormals(path);
    
    Path64 result = new Path64();
    
    if (group.endType == EndType.Polygon)
    {
        // 闭合多边形偏移
        OffsetPolygon(path, result, group.joinType);
    }
    else if (group.endType == EndType.Joined)
    {
        // 连接的开放路径
        OffsetOpenJoined(path, result, group.joinType);
    }
    else
    {
        // 开放路径
        OffsetOpenPath(path, result, group.joinType, group.endType);
    }
    
    if (result.Count > 0)
        _solution.Add(result);
}

16.8.2 构建法线

private void BuildNormals(Path64 path)
{
    _normals.Clear();
    int cnt = path.Count;
    
    for (int i = 0; i < cnt; i++)
    {
        int j = (i + 1) % cnt;
        
        double dx = path[j].X - path[i].X;
        double dy = path[j].Y - path[i].Y;
        double len = Math.Sqrt(dx * dx + dy * dy);
        
        if (len > 0)
        {
            dx /= len;
            dy /= len;
        }
        
        // 法线:垂直于边的单位向量
        _normals.Add(new PointD(dy, -dx));
    }
}

16.9 偏移计算

16.9.1 OffsetPolygon

private void OffsetPolygon(Path64 path, Path64 result, JoinType joinType)
{
    int pathLen = path.Count;
    
    for (int i = 0, k = pathLen - 1; i < pathLen; k = i++)
    {
        // 偏移当前边
        Point64 pt = new Point64(
            path[i].X + _normals[k].x * _groupDelta,
            path[i].Y + _normals[k].y * _groupDelta
        );
        result.Add(pt);
        
        // 处理拐角
        DoJoin(path, result, i, k, joinType);
    }
}

16.9.2 DoJoin 拐角处理

private void DoJoin(Path64 path, Path64 result, int j, int k, JoinType joinType)
{
    // 计算拐角角度
    double cosA = _normals[k].x * _normals[j].x + 
                  _normals[k].y * _normals[j].y;
    
    // 凹角检测
    double sinA = _normals[k].x * _normals[j].y - 
                  _normals[k].y * _normals[j].x;
    
    if (sinA * _groupDelta < 0)
    {
        // 凹角:添加交点
        Point64 pt = GetIntersection(_normals[k], _normals[j], 
            path[j], _groupDelta);
        result.Add(pt);
        return;
    }
    
    // 凸角:根据 JoinType 处理
    switch (joinType)
    {
        case JoinType.Miter:
            DoMiter(path, result, j, k, cosA);
            break;
            
        case JoinType.Square:
            DoSquare(path, result, j, k);
            break;
            
        case JoinType.Bevel:
            DoBevel(path, result, j, k);
            break;
            
        case JoinType.Round:
            DoRound(path, result, j, k, sinA);
            break;
    }
}

16.9.3 DoMiter

private void DoMiter(Path64 path, Path64 result, int j, int k, double cosA)
{
    // 检查是否超过斜接限制
    double q = _groupDelta / (1 + cosA);
    
    if (q > _mitLimSqr * _absGroupDelta)
    {
        // 超过限制,使用斜角
        DoBevel(path, result, j, k);
    }
    else
    {
        // 正常斜接
        result.Add(new Point64(
            path[j].X + (_normals[k].x + _normals[j].x) * q,
            path[j].Y + (_normals[k].y + _normals[j].y) * q
        ));
    }
}

16.9.4 DoRound

private void DoRound(Path64 path, Path64 result, int j, int k, double sinA)
{
    // 计算圆弧需要的步数
    double a = Math.Atan2(sinA, 
        _normals[k].x * _normals[j].x + _normals[k].y * _normals[j].y);
    int steps = Math.Max(2, (int)Math.Ceiling(_stepsPerRad * Math.Abs(a)));
    
    // 起始点
    double x = _normals[k].x;
    double y = _normals[k].y;
    
    for (int i = 0; i < steps; i++)
    {
        result.Add(new Point64(
            path[j].X + x * _groupDelta,
            path[j].Y + y * _groupDelta
        ));
        
        // 旋转
        double x2 = x * _stepCos - y * _stepSin;
        y = x * _stepSin + y * _stepCos;
        x = x2;
    }
}

16.10 开放路径偏移

16.10.1 OffsetOpenPath

private void OffsetOpenPath(Path64 path, Path64 result, 
    JoinType joinType, EndType endType)
{
    // 偏移正向
    OffsetLine(path, result, joinType);
    
    // 处理终点
    DoEndCap(path, result, endType, false);
    
    // 偏移反向
    OffsetLineReverse(path, result, joinType);
    
    // 处理起点
    DoEndCap(path, result, endType, true);
}

16.10.2 DoEndCap 端点处理

private void DoEndCap(Path64 path, Path64 result, EndType endType, bool isStart)
{
    int idx = isStart ? 0 : path.Count - 1;
    Point64 pt = path[idx];
    PointD normal = isStart ? 
        new PointD(-_normals[0].x, -_normals[0].y) : 
        _normals[path.Count - 2];
    
    switch (endType)
    {
        case EndType.Butt:
            // 平头:不添加额外点
            result.Add(new Point64(
                pt.X + normal.x * _groupDelta,
                pt.Y + normal.y * _groupDelta
            ));
            break;
            
        case EndType.Square:
            // 方形端
            result.Add(new Point64(
                pt.X + normal.x * _groupDelta - normal.y * _groupDelta,
                pt.Y + normal.y * _groupDelta + normal.x * _groupDelta
            ));
            result.Add(new Point64(
                pt.X - normal.x * _groupDelta - normal.y * _groupDelta,
                pt.Y - normal.y * _groupDelta + normal.x * _groupDelta
            ));
            break;
            
        case EndType.Round:
            // 圆形端
            DoRoundEnd(pt, normal, result);
            break;
    }
}

16.11 参数配置

16.11.1 MiterLimit

public double MiterLimit
{
    get => _miterLimit;
    set => _miterLimit = value > 0 ? value : 2.0;
}

MiterLimit 控制斜接角的最大延伸:

MiterLimit = 2(默认):角度约 60° 以下使用斜接
MiterLimit = 4:角度约 30° 以下使用斜接
MiterLimit = 1:总是使用斜角

16.11.2 ArcTolerance

public double ArcTolerance
{
    get => _arcTolerance;
    set => _arcTolerance = value > 0 ? value : 0.25;
}

ArcTolerance 控制圆弧的精度:

  • 值越小,圆弧越平滑
  • 值越大,圆弧越粗糙
  • 默认 0.25,即误差 0.25 单位

16.12 使用示例

16.12.1 基本使用

ClipperOffset co = new ClipperOffset();

// 添加多边形
Path64 polygon = new Path64 {
    new Point64(0, 0),
    new Point64(100, 0),
    new Point64(100, 100),
    new Point64(0, 100)
};
co.AddPath(polygon, JoinType.Round, EndType.Polygon);

// 执行膨胀 10 单位
Paths64 result = new Paths64();
co.Execute(10, result);

// 执行收缩 5 单位
co.Clear();
co.AddPaths(result, JoinType.Round, EndType.Polygon);
Paths64 shrunk = new Paths64();
co.Execute(-5, shrunk);

16.12.2 开放路径

ClipperOffset co = new ClipperOffset();

Path64 line = new Path64 {
    new Point64(0, 0),
    new Point64(100, 50),
    new Point64(200, 0)
};

// 圆角端点
co.AddPath(line, JoinType.Round, EndType.Round);

Paths64 result = new Paths64();
co.Execute(10, result);

16.13 本章小结

ClipperOffset 提供了强大的路径偏移功能:

  1. JoinType:四种拐角连接方式
  2. EndType:五种端点处理方式
  3. 参数控制:MiterLimit 和 ArcTolerance
  4. 自动合并:使用裁剪器合并重叠区域

合理配置参数可以满足各种偏移需求。


上一章:填充规则详解 返回目录 下一章:RectClip矩形裁剪优化