From 5825609c176b1724f84bfc685a9730b18fd433b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=82=E4=B9=8B=E9=92=B3?= Date: Sat, 25 Jul 2026 05:28:41 +0000 Subject: [PATCH] =?UTF-8?q?fix(v4.2):=20shared=20topology=20stability=20?= =?UTF-8?q?=E2=80=94=20fillet/chamfer/heal/featuretree=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- docs/20-差距分析-v4.2.md | 99 +++++++++++++++++++++++++++++++ src/brep/brep.cpp | 18 +++--- src/brep/modeling.cpp | 25 ++++---- tests/brep/test_brep_modeling.cpp | 8 +-- 4 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 docs/20-差距分析-v4.2.md diff --git a/docs/20-差距分析-v4.2.md b/docs/20-差距分析-v4.2.md new file mode 100644 index 0000000..b4f718d --- /dev/null +++ b/docs/20-差距分析-v4.2.md @@ -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 加速 diff --git a/src/brep/brep.cpp b/src/brep/brep.cpp index 1292538..8d4c4c7 100644 --- a/src/brep/brep.cpp +++ b/src/brep/brep.cpp @@ -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(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(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 diff --git a/src/brep/modeling.cpp b/src/brep/modeling.cpp index 46b0d71..6d3fc97 100644 --- a/src/brep/modeling.cpp +++ b/src/brep/modeling.cpp @@ -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 sorted_edges(lop.edges.size()); - // Rotate so we start after the filleted edge - int start_offset = static_cast((ei + 1) % lop.edges.size()); - for (size_t j = 0; j < lop.edges.size(); ++j) { - sorted_edges[j] = lop.edges[(start_offset + static_cast(j)) % lop.edges.size()]; + // Collect non-fillet edges in loop order (excluding the fillet edge) + std::vector 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(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 sorted_edges(lop.edges.size()); - int start_offset = static_cast((ei + 1) % lop.edges.size()); - for (size_t j = 0; j < lop.edges.size(); ++j) { - sorted_edges[j] = lop.edges[(start_offset + static_cast(j)) % lop.edges.size()]; + // Collect non-fillet edges in loop order (excluding the fillet edge) + std::vector 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(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; diff --git a/tests/brep/test_brep_modeling.cpp b/tests/brep/test_brep_modeling.cpp index 0e4cfac..518a11a 100644 --- a/tests/brep/test_brep_modeling.cpp +++ b/tests/brep/test_brep_modeling.cpp @@ -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) {