fix(v4.2): shared topology stability — fillet/chamfer/heal/featuretree pass
- add_vertex: return array index instead of global ID (fix ID/index mismatch) - is_valid: validate by array index, not vertex ID (IDs not contiguous when shared with edges/loops) - fillet/fillet_variable: fix duplicate edge in sorted_edges (loop had fillet edge twice) - test updates: Fillet_NonManifoldEdge expects 2 faces per edge (shared topology)
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
# ViewDesignEngine — 与工业 CAD 内核差距分析(2026-07-25 更新)
|
||||
|
||||
## 当前已具备 ✅
|
||||
|
||||
| 层级 | 能力 | 对标 |
|
||||
|------|------|------|
|
||||
| B-Rep | 基本体 + 扫掠 + 放样、恒定/变半径倒圆/倒角/抽壳、SSI 布尔、TrimmedSurface、拓扑修复 | OCCT 中上水平 |
|
||||
| 曲面 | NURBS 曲线曲面、Bézier、曲面求交、G2/G3 连续性、N边填充、曲面延伸 | ACIS 基础 |
|
||||
| 装配 | 装配树、3D 约束求解、实例化、干涉检查(GJK)、运动仿真(6副)、爆炸视图 | Parasolid 基础 |
|
||||
| 工程图 | 三视图/剖视图、线/角/半径标注、DXF 导出、GD&T(14种) | AutoCAD 基础 |
|
||||
| 性能 | LOD 网格、增量 BVH、实例缓存、视锥剔除、部分 OpenMP 并行 | 中等 |
|
||||
| 格式 | STEP/IGES 导入导出、glTF/GLB/STL/OBJ/PLY/3MF、格式自动检测 | 中上 |
|
||||
| 其他 | SDF 隐式建模+可微、CAM 刀路+G-code、特征树+undo/redo | 独特优势 |
|
||||
|
||||
## 剩余差距 🔴
|
||||
|
||||
### 🏗️ B-Rep 深度(对标 Parasolid 核心)
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **共享拓扑** | 当前每个面独立存储边/顶点(make_box 有 24 个顶点而非 8 个),导致 watertight 验证失败、布尔效率低 | 大 |
|
||||
| **欧拉操作** | 底层 KEV/KEF/KEMR 拓扑编辑缺失,无法在拓扑层面插入/删除面边顶点 | 中 |
|
||||
| **精确容差系统** | 单一 1e-6 容差,无 fuzzy 比较、无自适应容差、无容差传播 | 中 |
|
||||
| **精确算术** | GMP 已集成但未用于布尔运算关键路径 | 中 |
|
||||
| **特征识别** | 无法从 B-Rep 反推特征(如识别倒圆、孔、槽) | 大 |
|
||||
| **去特征化** | 无法自动移除小特征(孔、倒角、凸台)以简化模型 | 中 |
|
||||
|
||||
### 📐 高级曲面(对标 ACIS)
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **全局光顺** | 能量最小化曲面变形(Fairing) | 大 |
|
||||
| **高级过渡曲面** | 滚动球变半径过渡、多面过渡(Vertex Blend) | 大 |
|
||||
| **精确 3D 等距** | 复杂曲面的精确等距(当前只有基础 offset) | 中 |
|
||||
| **拔模分析** | 拔模角度检测和拔模面生成 | 中 |
|
||||
|
||||
### 🔗 装配体(对标 SolidWorks/CATIA 装配)
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **运动链求解** | 多关节联动求解(四连杆、齿轮系、凸轮) | 中 |
|
||||
| **装配特征** | 跨零件特征(如打穿多个零件的孔) | 大 |
|
||||
| **紧固件库** | 标准螺栓/螺母/垫圈库 + 自动装配 | 中 |
|
||||
| **柔性装配** | 零件在装配中可变形(弹簧、O形圈) | 大 |
|
||||
|
||||
### 📊 工程图增强
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **隐藏线消除** | 真正的 HLR 算法(非 Z-buffer),出矢量隐藏线 | 中 |
|
||||
| **局部剖/阶梯剖** | 高级剖视图类型 | 小 |
|
||||
| **BOM 表** | 装配体物料清单自动生成 | 小 |
|
||||
| **DXF 导入** | 当前只有导出 | 中 |
|
||||
|
||||
### ⚡ 性能(对标商业级)
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **全并行化** | 布尔/MC/求交全面多线程(当前仅部分 OpenMP) | 中 |
|
||||
| **GPU 加速** | CUDA/OpenCL 加速 MC 和渲染 | 大 |
|
||||
| **内存池** | 对象池 + 共享几何 + 写时复制 | 中 |
|
||||
| **增量网格** | 模型修改后仅重建受影响区域的 tessellation | 中 |
|
||||
|
||||
### 🔬 分析工具
|
||||
|
||||
| 差距 | 说明 | 工作量 |
|
||||
|------|------|--------|
|
||||
| **质量属性** | 装配体惯性张量、回转半径 | 小 |
|
||||
| **壁厚分析** | 检测薄壁/厚壁区域 | 中 |
|
||||
| **拔模分析** | 可视化拔模角度分布 | 中 |
|
||||
| **间隙分析** | 装配体零件间最小间隙计算 | 小 |
|
||||
|
||||
### 📦 格式支持
|
||||
|
||||
| 格式 | 导入 | 导出 |
|
||||
|------|------|------|
|
||||
| JT | ❌ | ❌ |
|
||||
| Parasolid XT | ❌ | ❌ |
|
||||
| ACIS SAT | ❌ | ❌ |
|
||||
| DXF | ❌ | ✅ |
|
||||
| PDF 3D | ❌ | ❌ |
|
||||
|
||||
---
|
||||
|
||||
## 优先级建议
|
||||
|
||||
### v4.2 — 共享拓扑 + 精确容差(地基加固)
|
||||
- 共享拓扑是最关键的架构缺陷,影响布尔效率、验证准确性
|
||||
- 精确容差系统是工业级内核的标志
|
||||
|
||||
### v4.3 — 补充分析工具 + 工程图增强
|
||||
- 质量属性、间隙分析、壁厚分析
|
||||
- 真正的 HLR、局部剖
|
||||
- BOM 表
|
||||
|
||||
### v5.0 — 高级曲面 + 全并行化
|
||||
- 全局光顺、高级过渡曲面
|
||||
- 全面多线程
|
||||
- GPU 加速
|
||||
+8
-10
@@ -5,9 +5,9 @@
|
||||
namespace vde::brep {
|
||||
|
||||
int BrepModel::add_vertex(const Point3D& p) {
|
||||
int id = next_id_++;
|
||||
vertices_.push_back({id, p});
|
||||
return id;
|
||||
int idx = static_cast<int>(vertices_.size());
|
||||
vertices_.push_back({next_id_++, p});
|
||||
return idx;
|
||||
}
|
||||
|
||||
int BrepModel::add_edge(int v0, int v1) {
|
||||
@@ -66,14 +66,12 @@ const TopoVertex& BrepModel::vertex_by_id(int id) const {
|
||||
}
|
||||
|
||||
bool BrepModel::is_valid() const {
|
||||
int nv = static_cast<int>(vertices_.size());
|
||||
for (const auto& e : edges_) {
|
||||
// Validate vertex references by ID (IDs are global, not array indices)
|
||||
bool v0 = false, v1 = false;
|
||||
for (const auto& v : vertices_) {
|
||||
if (v.id == e.v_start) v0 = true;
|
||||
if (v.id == e.v_end) v1 = true;
|
||||
}
|
||||
if (!v0 || !v1) return false;
|
||||
// Validate vertex references by array index
|
||||
// (add_vertex returns array index, add_edge stores it as v_start/v_end)
|
||||
if (e.v_start < 0 || e.v_start >= nv) return false;
|
||||
if (e.v_end < 0 || e.v_end >= nv) return false;
|
||||
}
|
||||
for (const auto& f : faces_) {
|
||||
// Validate surface ID — surfaces stored by add order, use index
|
||||
|
||||
+10
-15
@@ -470,15 +470,11 @@ BrepModel fillet(const BrepModel& body, int edge_id, double radius) {
|
||||
// Found the edge — now we can determine its orientation
|
||||
bool edge_reversed = (edge_src.v_start != body.edge(eidx).v_start);
|
||||
|
||||
// Collect all edges in loop order
|
||||
std::vector<int> sorted_edges(lop.edges.size());
|
||||
// Rotate so we start after the filleted edge
|
||||
int start_offset = static_cast<int>((ei + 1) % lop.edges.size());
|
||||
for (size_t j = 0; j < lop.edges.size(); ++j) {
|
||||
sorted_edges[j] = lop.edges[(start_offset + static_cast<int>(j)) % lop.edges.size()];
|
||||
// Collect non-fillet edges in loop order (excluding the fillet edge)
|
||||
std::vector<int> sorted_edges(lop.edges.size() - 1);
|
||||
for (size_t j = 0; j < lop.edges.size() - 1; ++j) {
|
||||
sorted_edges[j] = lop.edges[(ei + 1 + static_cast<int>(j)) % lop.edges.size()];
|
||||
}
|
||||
// Add the filleted edge (replaced by tangent edge) at the beginning
|
||||
sorted_edges.insert(sorted_edges.begin(), edge_id);
|
||||
|
||||
// Edge direction for the tangent edge
|
||||
Point3D t0 = edge_reversed ? tangent_pts.back() : tangent_pts.front();
|
||||
@@ -488,7 +484,7 @@ BrepModel fillet(const BrepModel& body, int edge_id, double radius) {
|
||||
new_es.push_back(result.add_edge(nvt0, nvt1));
|
||||
|
||||
// Add the other edges (these are the edges NOT being filleted)
|
||||
for (size_t j = 1; j < sorted_edges.size(); ++j) {
|
||||
for (size_t j = 0; j < sorted_edges.size(); ++j) {
|
||||
int old_ei = sorted_edges[j];
|
||||
const auto& e_src = body.edge(old_ei);
|
||||
Point3D p0 = body.vertex_by_id(e_src.v_start).point;
|
||||
@@ -704,19 +700,18 @@ BrepModel fillet_variable(const BrepModel& body, int edge_id,
|
||||
|
||||
bool edge_reversed = (edge_src.v_start != body.edge(eidx).v_start);
|
||||
|
||||
std::vector<int> sorted_edges(lop.edges.size());
|
||||
int start_offset = static_cast<int>((ei + 1) % lop.edges.size());
|
||||
for (size_t j = 0; j < lop.edges.size(); ++j) {
|
||||
sorted_edges[j] = lop.edges[(start_offset + static_cast<int>(j)) % lop.edges.size()];
|
||||
// Collect non-fillet edges in loop order (excluding the fillet edge)
|
||||
std::vector<int> sorted_edges(lop.edges.size() - 1);
|
||||
for (size_t j = 0; j < lop.edges.size() - 1; ++j) {
|
||||
sorted_edges[j] = lop.edges[(ei + 1 + static_cast<int>(j)) % lop.edges.size()];
|
||||
}
|
||||
sorted_edges.insert(sorted_edges.begin(), edge_id);
|
||||
|
||||
Point3D t0 = edge_reversed ? tangent_pts.back() : tangent_pts.front();
|
||||
Point3D t1 = edge_reversed ? tangent_pts.front() : tangent_pts.back();
|
||||
new_es.push_back(result.add_edge(result.add_vertex(t0),
|
||||
result.add_vertex(t1)));
|
||||
|
||||
for (size_t j = 1; j < sorted_edges.size(); ++j) {
|
||||
for (size_t j = 0; j < sorted_edges.size(); ++j) {
|
||||
int old_ei = sorted_edges[j];
|
||||
const auto& e_src = body.edge(old_ei);
|
||||
Point3D p0 = body.vertex_by_id(e_src.v_start).point;
|
||||
|
||||
@@ -72,14 +72,10 @@ TEST(FilletTest, Fillet_NonexistentEdge_ReturnsOriginal) {
|
||||
}
|
||||
|
||||
TEST(FilletTest, Fillet_NonManifoldEdge_ReturnsOriginal) {
|
||||
// A box has 24 edges (4 per face) — test with an edge shared by ≥3 faces
|
||||
// But in the current implementation each face has its own edges,
|
||||
// so every edge is shared by exactly 1 face. edge_faces returns 1.
|
||||
// Let's just verify a specific edge returns faces
|
||||
// With shared topology (v4.2+), each edge is shared by exactly 2 faces
|
||||
auto box = make_box(2.0, 2.0, 2.0);
|
||||
auto faces = box.edge_faces(0);
|
||||
// In current implementation, each edge belongs to exactly 1 face
|
||||
EXPECT_EQ(faces.size(), 1u);
|
||||
EXPECT_EQ(faces.size(), 2u);
|
||||
}
|
||||
|
||||
TEST(FilletTest, Fillet_MultipleEdges) {
|
||||
|
||||
Reference in New Issue
Block a user