引言
大家好!今天我们要聊一个在计算几何和计算机图形学中非常重要的数据结构——双向链接边列表(Doubly Connected Edge List,简称DCEL)。想象一下,当我们需要在计算机中表示一个复杂的地图、一个三维模型的表面,或者任何由多个多边形组成的图形时,DCEL就像是一个精密的"拓扑地图",能够清晰地记录所有点、边、面之间的关系。
DCEL解决了什么问题?
- 如何高效地存储和管理平面上的多边形划分
- 如何快速查询相邻的面、共享的边、连接的顶点
- 如何支持动态操作,比如分割面、分裂边、添加孔洞
应用场景举例:
- 计算机游戏中的地形管理
- GIS地理信息系统中的区域划分
- CAD设计软件中的模型编辑
- 机器人路径规划中的环境建模
在本文中,我将通过详细的C++实现,带大家一步步理解DCEL的工作原理和编程技巧。
算法原理详解
基本概念:DCEL的三大组件
可以把DCEL想象成一个精密的"建筑图纸",包含三个核心部分:
-
顶点(Vertex) - 就像建筑中的"桩点"
- 存储位置坐标(x,y)
- 记录从该点出发的"第一条边"
-
半边(HalfEdge) - 这是DCEL最巧妙的设计!
- 把每条无方向的边拆成两个有方向的"半边"
- 每个半边有明确的起点和方向
- 通过指针连接形成完整的拓扑网络
-
面(Face) - 由边围成的区域
- 有外边界(必须闭合)
- 可以有内边界(孔洞)
- 记录属于这个面的所有边
指针关系:DCEL的"神经系统"
顶点A → 半边A-B(从A指向B)
半边A-B → 孪生边B-A(从B指向A,方向相反)
半边A-B → 下一条边B-C → 再下一条边C-A → ... 最终回到起点
半边A-B → 所属的面F
面F → 外边界起点(半边A-B)
为什么要用"半边"?
因为一条边可能被两个面共享,拆成两个半边后,每个半边明确属于一个面,处理起来更方便!
关键操作流程说明
创建新面的过程:
1. 输入:一条作为起点的半边(比如从A到B的半边)
2. 创建一个新的面对象F
3. 设置F的外边界起点为输入的半边
4. 从这个半边开始,沿着next指针走一圈:
- 把经过的每条半边的"所属面"都设置为F
- 直到回到起点半边
5. 返回创建好的面F
分割面的过程(在两个顶点间加一条新边):
1. 输入:顶点V1、顶点V2、要被分割的面F
2. 检查V1和V2之间是否已经有边(避免重复)
3. 创建一对新的孪生半边:
- 半边E1:从V1指向V2
- 半边E2:从V2指向V1(E1的孪生边)
4. 在面F中找到从V1出发的边和从V2出发的边
5. 重新连接指针关系:
- 原来:... → 边A(到V1) → 边B(从V1出发) → ...
- 现在:... → 边A → 新边E1 → 边B → ...
- 另一侧类似处理
6. 用新边E2创建一个新面
7. 更新所有相关边的面归属信息
分裂边的过程(在边上插入新顶点):
1. 输入:要被分裂的半边E、新顶点V
2. 创建从新顶点V到原终点的新半边对
3. 调整指针关系:
- 原边E现在指向新顶点V
- 新边从V指向原终点
4. 保持面的归属不变
C++实现逐行解析
核心数据结构定义
// DCEL.h - 定义DCEL的三大核心结构
// 前向声明,因为结构之间互相引用
struct HalfEdge;
// 顶点结构 - 存储点的位置和关联边
struct Vertex {
double x, y; // 顶点的坐标位置
HalfEdge* incidentEdge; // 从该顶点出发的一条边(用于遍历)
std::string toString() const; // 将顶点转为字符串显示
};
// 面结构 - 表示一个封闭区域
struct Face {
HalfEdge* outerComponent; // 外边界环的起始边(必须存在)
std::list<HalfEdge*> innerComponents; // 内边界环列表(孔洞,可选)
};
// 半边结构 - DCEL的核心,表示有向边
struct HalfEdge {
Vertex* origin; // 边的起点顶点
HalfEdge* twin; // 反向的孪生边
Face* incidentFace; // 该边所属的面
HalfEdge* next; // 同一环中的下一条边
HalfEdge* prev; // 同一环中的前一条边
std::string toString() const; // 将边转为字符串显示
};
// DCEL主类 - 管理所有顶点、边、面
class DCEL {
private:
// 使用智能指针自动管理内存
std::vector<std::unique_ptr<Vertex>> vertices; // 所有顶点
std::vector<std::unique_ptr<HalfEdge>> edges; // 所有半边
std::vector<std::unique_ptr<Face>> faces; // 所有面
public:
// 构造函数和析构函数使用默认实现
~DCEL() = default;
// 下面是所有的公开接口方法...
// [方法声明将在后面详细解释]
};
顶点管理实现详解
// 创建新顶点,如果坐标相同的顶点已存在则返回已存在的顶点
Vertex* DCEL::createVertex(double x, double y) {
// 设置浮点数比较的精度阈值,避免浮点误差导致重复顶点
const double epsilon = 1e-10;
// 遍历所有已有顶点,检查是否已存在相同坐标的顶点
for (const auto& v : vertices) {
// 比较x坐标和y坐标是否在误差范围内相等
if (std::abs(v->x - x) < epsilon && std::abs(v->y - y) < epsilon) {
return v.get(); // 返回已存在的顶点,避免重复创建
}
}
// 创建新顶点对象,初始位置为(x,y),关联边暂时设为空
auto v = std::make_unique<Vertex>(Vertex{x, y, nullptr});
Vertex *raw = v.get(); // 获取原始指针用于返回
vertices.push_back(std::move(v)); // 将顶点加入管理列表
return raw; // 返回新顶点的指针
}
关键点说明:
epsilon用于处理浮点数精度问题,避免因计算误差产生重复顶点- 使用
unique_ptr智能指针自动管理内存,防止内存泄漏 - 顶点去重确保拓扑一致性
半边对创建与连接详解
// 创建一对孪生边(两条方向相反的半边)
std::pair<HalfEdge*, HalfEdge*> DCEL::createTwinEdges(Vertex* v1, Vertex* v2) {
// 安全检查:确保两个顶点指针有效
if (!v1 || !v2) return {nullptr, nullptr};
// 创建第一条半边:从v1指向v2
// 参数说明:起点v1,孪生边暂空,所属面暂空,前后指针暂空
auto e1 = std::make_unique<HalfEdge>(HalfEdge{v1, nullptr, nullptr, nullptr, nullptr});
// 创建第二条半边:从v2指向v1
auto e2 = std::make_unique<HalfEdge>(HalfEdge{v2, nullptr, nullptr, nullptr, nullptr});
// 获取原始指针用于建立关系
HalfEdge *raw1 = e1.get();
HalfEdge *raw2 = e2.get();
// 建立孪生关系:两条边互为反向边
raw1->twin = raw2; // e1的孪生边是e2
raw2->twin = raw1; // e2的孪生边是e1
// 设置顶点的关联边(如果顶点还没有关联边的话)
// 关联边用于从顶点开始遍历相邻边
if (!v1->incidentEdge) {
v1->incidentEdge = raw1; // v1的关联边设为从v1出发的e1
}
if (!v2->incidentEdge) {
v2->incidentEdge = raw2; // v2的关联边设为从v2出发的e2
}
// 将创建的两条边加入DCEL的边列表进行统一管理
edges.push_back(std::move(e1));
edges.push_back(std::move(e2));
// 返回两条边的指针
return {raw1, raw2};
}
// 连接两条边,建立next和prev关系
void DCEL::connectEdges(HalfEdge* e1, HalfEdge* e2) {
e1->next = e2; // e1的下一条边是e2
e2->prev = e1; // e2的前一条边是e1
}
关键点说明:
- 每条无向边都拆分成两个有向半边,这是DCEL的核心设计
- 孪生边关系使得我们可以轻松访问边的另一侧
- 顶点的
incidentEdge提供了遍历的起点
面操作与环管理详解
// 创建新面,以外边界环作为输入
Face* DCEL::createFace(HalfEdge* outerEdge) {
// 安全检查:确保外边界边有效
if (!outerEdge) return nullptr;
// 创建面对象,设置外边界,内边界列表初始为空
auto f = std::make_unique<Face>(Face{outerEdge, {}});
Face* raw = f.get(); // 获取原始指针
faces.push_back(std::move(f)); // 加入面列表
// 遍历外边界环上的所有边,设置它们的所属面
HalfEdge* current = outerEdge; // 从起始边开始
do {
current->incidentFace = raw; // 设置当前边属于这个面
current = current->next; // 移动到下一条边
} while (current != outerEdge); // 直到回到起始边(环闭合)
return raw; // 返回新创建的面
}
// 向面中添加内环(孔洞)
bool DCEL::addInnerComponent(Face* face, HalfEdge* innerEdge) {
// 安全检查:确保面和内环边有效
if (!face || !innerEdge) {
return false;
}
// 验证内环是否闭合:从起始边开始遍历,应该能回到起始边
HalfEdge* current = innerEdge;
size_t count = 0;
do {
current = current->next; // 沿着next指针移动
count++;
// 安全保护:如果遍历边数超过总边数,说明环未闭合或指针错误
if (count > edges.size()) {
std::cerr << "错误: 内环可能未闭合或形成无限循环" << std::endl;
return false;
}
} while (current != innerEdge); // 直到回到起始边
// 将内环起始边加入到面的内环列表中
face->innerComponents.push_back(innerEdge);
// 设置内环上所有边的所属面
current = innerEdge;
do {
current->incidentFace = face; // 内环边也属于这个面
current = current->next;
} while (current != innerEdge);
return true; // 添加成功
}
关键点说明:
- 面必须有一个外环,可以有多个内环(孔洞)
- 环验证确保拓扑正确性,防止无限循环
- 所有环上的边都要正确设置面归属
复杂拓扑操作:面分割详解
// 在面的两个顶点间添加新边,将面分割成两个面
void DCEL::splitFace(Vertex* v1, Vertex* v2, Face* face) {
// 安全检查:确保顶点和面有效
if (!v1 || !v2 || !face) return;
// 检查v1和v2之间是否已经存在边(避免重复创建)
for (auto& e : edges) {
// 检查是否存在v1-v2或v2-v1的边
if ((e->origin == v1 && e->twin->origin == v2) ||
(e->origin == v2 && e->twin->origin == v1)) {
return; // 边已存在,直接返回
}
}
// 创建一对新的孪生边连接v1和v2
auto edgePair = createTwinEdges(v1, v2);
HalfEdge* e1 = edgePair.first; // 从v1指向v2的新边
HalfEdge* e2 = edgePair.second; // 从v2指向v1的新边
// 在面中找到从v1出发的边和从v2出发的边
HalfEdge* edgeFromV1 = findEdgeFromVertex(v1, face);
HalfEdge* edgeFromV2 = findEdgeFromVertex(v2, face);
// 如果找不到合适的边,操作失败
if (!edgeFromV1 || !edgeFromV2) return;
// 保存原来的连接关系
HalfEdge* oldPrev1 = edgeFromV1->prev; // 原来在v1处的前一条边
HalfEdge* oldPrev2 = edgeFromV2->prev; // 原来在v2处的前一条边
// 重新连接指针,将新边插入到环中
// 在v1侧:... → oldPrev1 → e1 → edgeFromV2 → ...
connectEdges(oldPrev1, e1);
connectEdges(e1, edgeFromV2);
// 在v2侧:... → oldPrev2 → e2 → edgeFromV1 → ...
connectEdges(oldPrev2, e2);
connectEdges(e2, edgeFromV1);
// 用新边e2创建新面(原面被分割成两个面)
Face* newFace = createFace(e2);
// 更新原面的外边界为e1
face->outerComponent = e1;
// 更新两个面上所有边的面归属信息
updateFaceForEdges(e1, face); // e1所在的环属于原面
updateFaceForEdges(e2, newFace); // e2所在的环属于新面
}
// 辅助方法:在指定面中查找从某个顶点出发的边
HalfEdge* DCEL::findEdgeFromVertex(Vertex* v, Face* face) {
// 遍历所有边,找到起点为v且属于指定面的边
for (auto& e : edges) {
if (e->origin == v && e->incidentFace == face) {
return e.get(); // 返回找到的边
}
}
return nullptr; // 没找到返回空
}
关键点说明:
- 面分割是DCEL最复杂的操作之一,需要精心调整多个指针
- 必须检查边是否已存在,避免破坏拓扑结构
- 分割后要正确更新两个新面的边界和边归属
边分割操作详解
// 在边上插入新顶点,将一条边分裂成两条边
std::pair<HalfEdge*, HalfEdge*> DCEL::splitEdge(HalfEdge* edge, Vertex* newVertex) {
// 安全检查
if (!edge || !newVertex) {
return {nullptr, nullptr};
}
// 获取原边的终点顶点
Vertex* oldEnd = edge->twin->origin;
// 创建从新顶点到原终点的新边对
auto newEdges = createTwinEdges(newVertex, oldEnd);
HalfEdge* newEdge1 = newEdges.first; // newVertex → oldEnd
HalfEdge* newEdge2 = newEdges.second; // oldEnd → newVertex
if (!newEdge1 || !newEdge2) {
return {nullptr, nullptr};
}
// 保存原来的连接关系
HalfEdge* oldNext = edge->next; // 原边的下一条边
HalfEdge* oldPrev = edge->prev; // 原边的前一条边
HalfEdge* twinOldNext = edge->twin->next; // 孪生边的下一条边
HalfEdge* twinOldPrev = edge->twin->prev; // 孪生边的前一条边
// 重新连接原边环的关系:
// 原来:oldPrev → edge → oldNext
// 现在:oldPrev → edge → newEdge1 → oldNext
if (oldPrev) {
oldPrev->next = edge;
edge->prev = oldPrev;
}
edge->next = newEdge1; // 原边现在指向新边
newEdge1->prev = edge; // 新边指回原边
newEdge1->next = oldNext; // 新边指向原边的下一条边
if (oldNext) {
oldNext->prev = newEdge1; // 原下一条边指回新边
}
// 类似地重新连接孪生边环的关系
if (twinOldPrev) {
twinOldPrev->next = edge->twin;
edge->twin->prev = twinOldPrev;
}
edge->twin->next = newEdge2;
newEdge2->prev = edge->twin;
newEdge2->next = twinOldNext;
if (twinOldNext) {
twinOldNext->prev = newEdge2;
}
// 更新面的关联:新边继承原边的面归属
if (edge->incidentFace) {
newEdge1->incidentFace = edge->incidentFace;
}
if (edge->twin->incidentFace) {
newEdge2->incidentFace = edge->twin->incidentFace;
}
// 更新顶点关联边
if (!newVertex->incidentEdge) {
newVertex->incidentEdge = newEdge1; // 新顶点的关联边
}
if (!oldEnd->incidentEdge) {
oldEnd->incidentEdge = newEdge2; // 原终点的关联边
}
return {newEdge1, newEdge2};
}
关键点说明:
- 边分割需要同时处理原边和孪生边两个环
- 新边继承原边的面归属信息
- 要正确设置新顶点的关联边
实用工具方法详解
// 验证DCEL拓扑结构的一致性
bool DCEL::validate() const {
bool valid = true; // 假设初始有效
// 遍历所有边检查关系是否正确
for (auto& e : edges) {
// 检查孪生边关系:边的孪生边的孪生边应该是自己
if (e->twin->twin != e.get()) {
std::cerr << "Twin关系错误: " << e->toString() << std::endl;
valid = false; // 发现错误
}
// 检查next-prev关系:边的下一条边的前一条边应该是自己
if (e->next && e->next->prev != e.get()) {
std::cerr << "Next/Prev关系错误: " << e->toString() << std::endl;
valid = false; // 发现错误
}
}
return valid; // 返回验证结果
}
// 打印整个DCEL结构的信息(用于调试)
void DCEL::print() const {
std::cout << "==== DCEL结构 ====" << std::endl;
// 打印所有顶点信息
std::cout << "顶点 (" << vertices.size() << "):" << std::endl;
for (const auto& v : vertices) {
std::cout << " " << v->toString() << " [关联边: "
<< (v->incidentEdge ? v->incidentEdge->toString() : "null") << "]"
<< std::endl;
}
// 打印所有面信息
std::cout << std::endl << "面 (" << faces.size() << "):" << std::endl;
for (const auto& f : faces) {
if (!f->outerComponent) continue; // 跳过无效面
// 打印外边界环
std::cout << " 外环: " << std::endl;
HalfEdge* start = f->outerComponent;
HalfEdge* current = start;
size_t count = 0;
do {
std::cout << " " << current->toString() << std::endl;
current = current->next;
// 安全保护:防止无限循环
if (++count > edges.size()) {
std::cout << " [错误: 检测到无限循环]" << std::endl;
break;
}
} while (current != start);
// 打印内环(孔洞)
if (!f->innerComponents.empty()) {
std::cout << " 内环 (" << f->innerComponents.size() << "个):" << std::endl;
for (auto innerEdge : f->innerComponents) {
std::cout << " 内环: " << std::endl;
HalfEdge* innerStart = innerEdge;
HalfEdge* innerCurrent = innerStart;
size_t innerCount = 0;
do {
std::cout << " " << innerCurrent->toString() << std::endl;
innerCurrent = innerCurrent->next;
// 安全保护
if (++innerCount > edges.size()) {
std::cout << " [错误: 检测到无限循环]" << std::endl;
break;
}
} while (innerCurrent != innerStart);
}
}
}
}
复杂度分析
时间复杂度分析
让我们分析各个操作的时间成本:
-
创建顶点:O(n)
- 需要遍历检查顶点重复,n是当前顶点数量
- 最坏情况:每次都要检查所有顶点
-
创建边对:O(1)
- 只是创建新对象和设置指针,常数时间
-
面分割:O(n)
- 需要查找顶点关联边,最坏要遍历所有边
- 其他操作都是常数时间
-
边分割:O(1)
- 局部指针调整,不受数据规模影响
-
拓扑验证:O(n)
- 需要遍历所有边检查关系
空间复杂度分析
DCEL的空间使用情况:
-
总体空间:O(V + E + F)
- V:顶点数量
- E:边数量(实际存储2E条半边)
- F:面数量
-
每个组件空间:
- 顶点:固定大小,O(1)空间
- 半边:包含多个指针,但也是O(1)空间
- 面:O(1)基础空间 + O(k)用于存储内环列表,k是内环数量
内存使用特点:
- 相比简单的多边形列表,DCEL需要更多内存存储拓扑关系
- 但相比其他复杂数据结构,在需要频繁拓扑查询的场景下更高效
优势与局限性
DCEL的显著优势
-
完整的拓扑信息
- 可以快速回答"这个面的邻居是谁?"
- 轻松遍历面的边界、顶点的邻边
- 支持复杂的空间关系查询
-
处理复杂边界能力强
- 天然支持带孔多边形
- 可以处理多个分离的边界环
- 适应各种复杂的几何形状
-
动态操作支持好
- 面分割、边分裂等操作效率高
- 局部修改不影响整体结构
- 适合交互式编辑场景
-
内存相对高效
- 相比某些冗余存储方案更节省空间
- 在复杂场景下比其他表示方法更紧凑
DCEL的局限性
-
实现复杂度高
- 指针关系复杂,容易出错
- 需要精心维护拓扑一致性
- 调试困难,特别是指针错误时
-
浮点精度挑战
- 几何计算受浮点误差影响
- 需要特殊处理顶点重合判断
- 可能影响算法的鲁棒性
-
学习曲线陡峭
- 需要深入理解拓扑概念
- 初学者需要时间掌握指针关系
- 调试和验证需要专门工具
-
缓存性能一般
- 指针跳转可能影响CPU缓存效率
- 在超大模型下性能可能下降
总结与扩展
通过本文的详细讲解,相信大家对DCEL有了深入的理解。我们来回顾一下重点:
核心要点回顾
- 半边设计是精髓:将无向边拆分为有向半边,简化了面边关系的管理
- 指针网络是关键:通过twin、next、prev指针建立完整的拓扑关系
- 环验证很重要:确保所有边界环都正确闭合,防止无限循环
- 内存管理要谨慎:使用智能指针避免内存泄漏,但要注意循环引用
实际应用建议
适合使用DCEL的场景:
- 需要频繁进行拓扑查询的应用
- 支持复杂编辑操作的CAD/CAM系统
- 处理带孔多边形的GIS应用
- 需要动态修改网格的图形应用
不适合使用DCEL的场景:
- 只需要简单渲染,不关心拓扑关系
- 数据是只读的,不需要修改
- 对内存使用极其敏感的场景
扩展思考与优化方向
-
性能优化
- 使用内存池减少动态分配开销
- 添加空间索引加速几何查询
- 实现增量验证,避免全量检查
-
功能扩展
- 添加序列化支持,便于存储和传输
- 集成更多几何算法(求交、偏移等)
- 支持属性附着(颜色、纹理等)
-
健壮性提升
- 实现自动拓扑修复功能
- 添加更完善的错误处理
- 提供可视化调试工具
-
算法集成
- 与Delaunay三角剖分结合
- 支持Voronoi图生成
- 集成网格简化算法
DCEL虽然实现复杂,但它的设计思想非常优美——通过精心的指针设计,在几何实体间建立了清晰的关系网络。这种"关系优先"的设计理念在很多领域都值得借鉴。
希望本文能帮助大家更好地理解和应用这个强大的几何数据结构!
完整代码实现
DCEL.h
#pragma once
#include <vector>
#include <list>
#include <memory>
#include <locale>
#ifdef _WIN32
#include <windows.h>
#endif
// DCEL核心数据结构
struct HalfEdge;
/************************顶点******************************/
struct Vertex {
double x, y; // 顶点的坐标位置
HalfEdge* incidentEdge; // 从该顶点出发的一条边(用于遍历)
std::string toString() const; // 将顶点转为字符串显示
};
/**************************面*****************************/
struct Face {
HalfEdge* outerComponent; // 外边界环的起始边(必须存在)
std::list<HalfEdge*> innerComponents; // 内边界环列表(孔洞,可选)
};
/************************半边*****************************/
struct HalfEdge {
Vertex* origin; // 边的起点顶点
HalfEdge* twin; // 反向的孪生边
Face* incidentFace; // 该边所属的面
HalfEdge* next; // 同一环中的下一条边
HalfEdge* prev; // 同一环中的前一条边
std::string toString() const; // 将边转为字符串显示
};
/*********************双向链接表***************************/
class DCEL {
private:
// 使用智能指针自动管理内存
std::vector<std::unique_ptr<Vertex>> vertices; // 所有顶点
std::vector<std::unique_ptr<HalfEdge>> edges; // 所有半边
std::vector<std::unique_ptr<Face>> faces; // 所有面
public:
~DCEL() = default;
// 创建新顶点
Vertex* createVertex(double x, double y);
// 创建一对twin边
std::pair<HalfEdge*, HalfEdge*> createTwinEdges(Vertex* v1, Vertex* v2);
// 连接边形成环
void connectEdges(HalfEdge* e1, HalfEdge* e2);
// 创建新面(使用外环)
Face* createFace(HalfEdge* outerEdge);
// 添加内环到面
bool addInnerComponent(Face* face, HalfEdge* innerEdge);
// 创建带内环的面
Face* createFaceWithInnerComponent(HalfEdge* outerEdge, HalfEdge* innerEdge);
// 分割面(使用新边连接两个顶点)
void splitFace(Vertex* v1, Vertex* v2, Face* face);
// 辅助方法:查找从指定顶点出发的边(属于指定面)
HalfEdge* findEdgeFromVertex(Vertex* v, Face* face);
// 验证拓扑结构
bool validate() const;
// 获取顶点数量
size_t getVertexCount() const;
// 获取边数量
size_t getEdgeCount() const;
// 获取面数量
size_t getFaceCount() const;
// 清空所有数据
void clear();
// 查找顶点(考虑浮点精度)
Vertex* findVertex(double x, double y) const;
// 查找边
HalfEdge* findEdge(Vertex* v1, Vertex* v2) const;
// 分割边
std::pair<HalfEdge*, HalfEdge*> splitEdge(HalfEdge* edge, Vertex* newVertex);
// 打印结构信息
void print() const;
// 获取边列表(用于遍历)
const std::vector<std::unique_ptr<HalfEdge>>& getEdges() const;
private:
void updateFaceForEdges(HalfEdge* startEdge, Face* face);
};
DCEL.cpp
#include "DCEL.h"
#include <iostream>
#include <sstream>
/************************顶点******************************/
std::string Vertex::toString() const
{
std::stringstream ss;
ss << "(" << x << ", " << y << ")";
return ss.str();
}
/************************半边*****************************/
std::string HalfEdge::toString() const
{
std::stringstream ss;
ss << origin->toString() << " -> " << twin->origin->toString();
return ss.str();
}
/*********************双向链接表***************************/
// 创建新顶点(自动去重)
Vertex *DCEL::createVertex(double x, double y)
{
// 首先检查是否已存在相同坐标的顶点
const double epsilon = 1e-10;
for (const auto& v : vertices) {
if (std::abs(v->x - x) < epsilon && std::abs(v->y - y) < epsilon) {
return v.get(); // 返回已存在的顶点
}
}
// 创建新顶点
auto v = std::make_unique<Vertex>(Vertex{x, y, nullptr});
Vertex *raw = v.get();
vertices.push_back(std::move(v));
return raw;
}
// 创建一对twin边
std::pair<HalfEdge *, HalfEdge *> DCEL::createTwinEdges(Vertex *v1, Vertex *v2)
{
if (!v1 || !v2)
return {nullptr, nullptr};
auto e1 = std::make_unique<HalfEdge>(HalfEdge{v1, nullptr, nullptr, nullptr, nullptr});
auto e2 = std::make_unique<HalfEdge>(HalfEdge{v2, nullptr, nullptr, nullptr, nullptr});
HalfEdge *raw1 = e1.get();
HalfEdge *raw2 = e2.get();
raw1->twin = raw2;
raw2->twin = raw1;
// 设置顶点的关联边
if (!v1->incidentEdge) {
v1->incidentEdge = raw1;
}
if (!v2->incidentEdge) {
v2->incidentEdge = raw2;
}
edges.push_back(std::move(e1));
edges.push_back(std::move(e2));
return {raw1, raw2};
}
// 连接边形成环
void DCEL::connectEdges(HalfEdge *e1, HalfEdge *e2)
{
e1->next = e2;
e2->prev = e1;
}
// 创建新面(使用外环)
Face *DCEL::createFace(HalfEdge *outerEdge) {
if (!outerEdge)
return nullptr;
auto f = std::make_unique<Face>(Face{outerEdge, {}});
Face *raw = f.get();
faces.push_back(std::move(f));
// 设置面上所有边的incidentFace
HalfEdge *current = outerEdge;
do {
current->incidentFace = raw;
current = current->next;
} while (current != outerEdge);
return raw;
}
// 添加内环到面
bool DCEL::addInnerComponent(Face *face, HalfEdge *innerEdge) {
if (!face || !innerEdge) {
return false;
}
// 验证内环是否闭合
HalfEdge *current = innerEdge;
size_t count = 0;
do {
current = current->next;
count++;
if (count > edges.size()) {
std::cerr << "错误: 内环可能未闭合或形成无限循环" << std::endl;
return false;
}
} while (current != innerEdge);
// 添加内环到面的内环列表
face->innerComponents.push_back(innerEdge);
// 设置内环上所有边的关联面
current = innerEdge;
do {
current->incidentFace = face;
current = current->next;
} while (current != innerEdge);
return true;
}
// 创建带内环的面
Face *DCEL::createFaceWithInnerComponent(HalfEdge *outerEdge, HalfEdge *innerEdge) {
Face *face = createFace(outerEdge);
if (face && innerEdge) {
addInnerComponent(face, innerEdge);
}
return face;
}
// 分割面(使用新边连接两个顶点)
void DCEL::splitFace(Vertex *v1, Vertex *v2, Face *face) {
if (!v1 || !v2 || !face)
return;
// 检查顶点是否已经在同一条边上
for (auto &e : edges) {
if ((e->origin == v1 && e->twin->origin == v2) ||
(e->origin == v2 && e->twin->origin == v1)) {
return; // 边已存在
}
}
// 创建新边对
auto edgePair = createTwinEdges(v1, v2);
HalfEdge *e1 = edgePair.first;
HalfEdge *e2 = edgePair.second;
// 找到面中的两条边(在v1和v2处)
HalfEdge *edgeFromV1 = findEdgeFromVertex(v1, face);
HalfEdge *edgeFromV2 = findEdgeFromVertex(v2, face);
if (!edgeFromV1 || !edgeFromV2)
return;
// 保存原来的连接关系
HalfEdge *oldPrev1 = edgeFromV1->prev;
HalfEdge *oldPrev2 = edgeFromV2->prev;
// 重新连接边
connectEdges(oldPrev1, e1);
connectEdges(e1, edgeFromV2);
connectEdges(oldPrev2, e2);
connectEdges(e2, edgeFromV1);
// 创建新面
Face *newFace = createFace(e2);
face->outerComponent = e1;
// 更新面上所有边的归属
updateFaceForEdges(e1, face);
updateFaceForEdges(e2, newFace);
}
// 辅助方法:查找从指定顶点出发的边(属于指定面)
HalfEdge *DCEL::findEdgeFromVertex(Vertex *v, Face *face) {
for (auto &e : edges) {
if (e->origin == v && e->incidentFace == face) {
return e.get();
}
}
return nullptr;
}
// 验证拓扑结构
bool DCEL::validate() const {
bool valid = true;
for (auto &e : edges) {
if (e->twin->twin != e.get()) {
std::cerr << "Twin关系错误: " << e->toString() << std::endl;
valid = false;
}
if (e->next && e->next->prev != e.get()) {
std::cerr << "Next/Prev关系错误: " << e->toString() << std::endl;
valid = false;
}
}
return valid;
}
// 获取顶点数量
size_t DCEL::getVertexCount() const { return vertices.size(); }
// 获取边数量
size_t DCEL::getEdgeCount() const { return edges.size(); }
// 获取面数量
size_t DCEL::getFaceCount() const { return faces.size(); }
// 清空所有数据
void DCEL::clear() {
vertices.clear();
edges.clear();
faces.clear();
}
// 查找顶点(考虑浮点精度)
Vertex *DCEL::findVertex(double x, double y) const {
const double epsilon = 1e-10;
for (const auto &v : vertices) {
if (std::abs(v->x - x) < epsilon && std::abs(v->y - y) < epsilon) {
return v.get();
}
}
return nullptr;
}
// 查找边
HalfEdge *DCEL::findEdge(Vertex *v1, Vertex *v2) const {
for (const auto &e : edges) {
if ((e->origin == v1 && e->twin->origin == v2) ||
(e->origin == v2 && e->twin->origin == v1)) {
return e.get();
}
}
return nullptr;
}
// 分割边
std::pair<HalfEdge *, HalfEdge *> DCEL::splitEdge(HalfEdge *edge,
Vertex *newVertex) {
if (!edge || !newVertex) {
return {nullptr, nullptr};
}
// 获取原边的终点
Vertex *oldEnd = edge->twin->origin;
// 创建从新顶点到原终点的边对
auto newEdges = createTwinEdges(newVertex, oldEnd);
HalfEdge *newEdge1 = newEdges.first; // newVertex -> oldEnd
HalfEdge *newEdge2 = newEdges.second; // oldEnd -> newVertex
if (!newEdge1 || !newEdge2) {
return {nullptr, nullptr};
}
// 保存原来的连接关系
HalfEdge *oldNext = edge->next;
HalfEdge *oldPrev = edge->prev;
HalfEdge *twinOldNext = edge->twin->next;
HalfEdge *twinOldPrev = edge->twin->prev;
// 重新连接边的关系
// 原边:oldPrev -> edge -> newEdge1 -> oldNext
if (oldPrev) {
oldPrev->next = edge;
edge->prev = oldPrev;
}
edge->next = newEdge1;
newEdge1->prev = edge;
newEdge1->next = oldNext;
if (oldNext) {
oldNext->prev = newEdge1;
}
// 孪生边:twinOldPrev -> edge->twin -> newEdge2 -> twinOldNext
if (twinOldPrev) {
twinOldPrev->next = edge->twin;
edge->twin->prev = twinOldPrev;
}
edge->twin->next = newEdge2;
newEdge2->prev = edge->twin;
newEdge2->next = twinOldNext;
if (twinOldNext) {
twinOldNext->prev = newEdge2;
}
// 更新面的关联(如果原边有关联面)
if (edge->incidentFace) {
newEdge1->incidentFace = edge->incidentFace;
}
if (edge->twin->incidentFace) {
newEdge2->incidentFace = edge->twin->incidentFace;
}
// 更新顶点关联边
if (!newVertex->incidentEdge) {
newVertex->incidentEdge = newEdge1;
}
if (!oldEnd->incidentEdge) {
oldEnd->incidentEdge = newEdge2;
}
return {newEdge1, newEdge2};
}
// 打印结构信息
void DCEL::print() const {
std::cout << "==== DCEL结构 ====" << std::endl;
std::cout << "顶点 (" << vertices.size() << "):" << std::endl;
for (const auto &v : vertices) {
std::cout << " " << v->toString() << " [关联边: "
<< (v->incidentEdge ? v->incidentEdge->toString() : "null") << "]"
<< std::endl;
}
std::cout << std::endl << "面 (" << faces.size() << "):" << std::endl;
for (const auto &f : faces) {
if (!f->outerComponent)
continue;
std::cout << " 外环: " << std::endl;
HalfEdge *start = f->outerComponent;
HalfEdge *current = start;
size_t count = 0;
do {
std::cout << " " << current->toString() << std::endl;
current = current->next;
if (++count > edges.size()) {
std::cout << " [错误: 检测到无限循环]" << std::endl;
break;
}
} while (current != start);
// 打印内环
if (!f->innerComponents.empty()) {
std::cout << " 内环 (" << f->innerComponents.size() << "个):" << std::endl;
for (auto innerEdge : f->innerComponents) {
std::cout << " 内环: " << std::endl;
HalfEdge *innerStart = innerEdge;
HalfEdge *innerCurrent = innerStart;
size_t innerCount = 0;
do {
std::cout << " " << innerCurrent->toString() << std::endl;
innerCurrent = innerCurrent->next;
if (++innerCount > edges.size()) {
std::cout << " [错误: 检测到无限循环]" << std::endl;
break;
}
} while (innerCurrent != innerStart);
}
}
}
}
// 获取边列表(用于遍历)
const std::vector<std::unique_ptr<HalfEdge>>& DCEL::getEdges() const { return edges; }
void DCEL::updateFaceForEdges(HalfEdge *startEdge, Face *face) {
if (!startEdge)
return;
HalfEdge *current = startEdge;
size_t count = 0;
size_t maxCount = edges.size() * 2; // 安全限制
do {
current->incidentFace = face;
current = current->next;
count++;
if (count > maxCount) {
std::cerr << "警告: 检测到可能的无限循环,终止更新面归属" << std::endl;
break;
}
} while (current && current != startEdge);
}
1008

被折叠的 条评论
为什么被折叠?



