惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LangChain Blog
W
WeLiveSecurity
P
Proofpoint News Feed
月光博客
月光博客
NISL@THU
NISL@THU
L
LINUX DO - 最新话题
Webroot Blog
Webroot Blog
T
Threatpost
Y
Y Combinator Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Threat Research - Cisco Blogs
Vercel News
Vercel News
Jina AI
Jina AI
阮一峰的网络日志
阮一峰的网络日志
S
Schneier on Security
J
Java Code Geeks
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
小众软件
小众软件
MyScale Blog
MyScale Blog
N
News and Events Feed by Topic
Stack Overflow Blog
Stack Overflow Blog
有赞技术团队
有赞技术团队
The Hacker News
The Hacker News
Schneier on Security
Schneier on Security
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Help Net Security
Help Net Security
Recent Announcements
Recent Announcements
S
Security @ Cisco Blogs
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Securelist
T
The Exploit Database - CXSecurity.com
云风的 BLOG
云风的 BLOG
C
Cisco Blogs
雷峰网
雷峰网
量子位
Google DeepMind News
Google DeepMind News
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Spread Privacy
Spread Privacy
L
Lohrmann on Cybersecurity
I
Intezer
T
The Blog of Author Tim Ferriss
G
GRAHAM CLULEY
D
DataBreaches.Net
V
Vulnerabilities – Threatpost
P
Privacy & Cybersecurity Law Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
罗磊的独立博客

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第16章:ClipperOffset 偏移类详解
我才是银古 · 2026-06-24 · via 博客园 - 我才是银古

第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矩形裁剪优化