第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 提供了强大的路径偏移功能:
- JoinType:四种拐角连接方式
- EndType:五种端点处理方式
- 参数控制:MiterLimit 和 ArcTolerance
- 自动合并:使用裁剪器合并重叠区域
合理配置参数可以满足各种偏移需求。
| 上一章:填充规则详解 | 返回目录 | 下一章:RectClip矩形裁剪优化 |