commit 02d0520aa5a00980d582d1d69e1cefa86d23b89e Author: ViewDesignEngine Date: Thu Jul 23 05:27:51 2026 +0000 v0.1.0: 初始工程骨架 — 7模块 40头文件 32源文件 17文档 Apache-2.0 diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..fe72dbe --- /dev/null +++ b/.clang-format @@ -0,0 +1,33 @@ +BasedOnStyle: Google +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +ColumnLimit: 100 +AccessModifierOffset: -4 +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +PointerAlignment: Left +DerivePointerAlignment: false +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: Never + AfterFunction: true + AfterNamespace: false + AfterStruct: true + AfterUnion: true + BeforeElse: false +NamespaceIndentation: None +FixNamespaceComments: true +SortIncludes: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^<.*\.h>' + Priority: 1 + - Regex: '^<.*>' + Priority: 2 + - Regex: '^"vde/.*"' + Priority: 3 + - Regex: '.*' + Priority: 4 diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..da2d11f --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,44 @@ +Checks: > + -*, + bugprone-*, + cert-*, + clang-analyzer-*, + cppcoreguidelines-*, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-owning-memory, + google-*, + -google-readability-todo, + misc-*, + -misc-non-private-member-variables-in-classes, + modernize-*, + -modernize-use-trailing-return-type, + performance-*, + portability-*, + readability-*, + -readability-magic-numbers, + -readability-identifier-length + +CheckOptions: + - key: readability-identifier-naming.NamespaceCase + value: lower_case + - key: readability-identifier-naming.ClassCase + value: CamelCase + - key: readability-identifier-naming.StructCase + value: CamelCase + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.MemberCase + value: lower_case + - key: readability-identifier-naming.MemberSuffix + value: _ + - key: readability-identifier-naming.EnumConstantCase + value: CamelCase + - key: readability-identifier-naming.EnumConstantPrefix + value: k + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE + +WarningsAsErrors: "" diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..644d60c --- /dev/null +++ b/.clangd @@ -0,0 +1,8 @@ +CompileFlags: + Add: [-std=c++17] + CompilationDatabase: build + +Diagnostics: + ClangTidy: + Add: [performance-*, bugprone-*, modernize-*] + Remove: [modernize-use-trailing-return-type] diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..da38dd2 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main] + +jobs: + build-test: + name: Build & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Eigen + run: | + apt-get update -qq + apt-get install -y -qq libeigen3-dev + + - name: Configure (Debug) + run: cmake -B build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON + + - name: Build + run: cmake --build build -j$(nproc) + + - name: Test + run: cd build && ctest --output-on-failure + + release-build: + name: Release Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Eigen + run: | + apt-get update -qq + apt-get install -y -qq libeigen3-dev + + - name: Configure (Release) + run: cmake -B build -DCMAKE_BUILD_TYPE=Release + + - name: Build + run: cmake --build build -j$(nproc) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..779ea3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +build/ +.cache/ +*.swp +*.swo +*~ +.DS_Store +compile_commands.json +*.gcda +*.gcno +*.gcov +coverage_report/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab36fe2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,23 @@ +# 更新日志 + +## [0.1.0] — 2026-07-23 + +### 新增 +- 基础设施层(foundation):数学类型、公差系统、精确谓词、OBJ/STL 读写 +- 几何核心层(core):点、线、面、三角形、AABB、几何变换、距离计算、凸包 +- 曲线曲面层(curves):Bezier/B-Spline/NURBS 曲线与曲面、离散化 +- 网格处理层(mesh):半边数据结构、Delaunay 2D 三角剖分 +- 空间索引层(spatial):BVH(SAH 构建) +- 碰撞检测层(collision):GJK 算法、Möller-Trumbore 射线-三角形相交 +- 布尔运算层(boolean):2D 多边形 Sutherland-Hodgman 裁剪 +- 构建系统:CMake + FetchContent(Eigen, GTest 自动下载) +- 测试框架:Google Test,11 个测试文件 +- 示例程序:6 个可运行示例 + +### 待完成 +- 网格简化(QEM)、网格平滑(Laplacian/Taubin) +- 3D 网格布尔运算 +- Octree、KD-Tree、R-Tree 完整实现 +- 3D Delaunay 剖分 +- 3D 凸包(QuickHull) +- B-Rep 支持 diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f3eab9b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.16) +project(ViewDesignEngine VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ── 选项 ────────────────────────────────────────── +option(BUILD_TESTS "Build unit tests" ON) +option(BUILD_EXAMPLES "Build examples" ON) +option(BUILD_BENCHMARKS "Build benchmarks" OFF) +option(BUILD_SHARED_LIBS "Build shared library" OFF) +option(ENABLE_SANITIZERS "Enable ASan/UBSan" OFF) +option(ENABLE_LTO "Enable LTO" ON) + +# ── 编译设置 ────────────────────────────────────── +include(cmake/CompilerSettings.cmake) + +# ── 依赖 ────────────────────────────────────────── +find_package(Eigen3 3.3 QUIET) +if(NOT Eigen3_FOUND) + message(STATUS "Eigen3 not found — using FetchContent") + include(FetchContent) + FetchContent_Declare(Eigen3 + URL https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz + ) + FetchContent_MakeAvailable(Eigen3) +endif() + +# ── 子目录 ──────────────────────────────────────── +add_subdirectory(src) + +# ── 测试 ────────────────────────────────────────── +if(BUILD_TESTS) + enable_testing() + find_package(GTest QUIET) + if(NOT GTest_FOUND) + include(FetchContent) + FetchContent_Declare(googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz + ) + set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) + endif() + add_subdirectory(tests) +endif() + +# ── 示例 ────────────────────────────────────────── +if(BUILD_EXAMPLES) + add_subdirectory(examples) +endif() + +# ── 性能测试 ────────────────────────────────────── +if(BUILD_BENCHMARKS) + find_package(benchmark QUIET) + if(NOT benchmark_FOUND) + include(FetchContent) + FetchContent_Declare(googlebenchmark + URL https://github.com/google/benchmark/archive/refs/tags/v1.8.0.tar.gz + ) + set(BENCHMARK_ENABLE_TESTING OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googlebenchmark) + endif() + add_subdirectory(benchmarks) +endif() + +# ── 安装 ────────────────────────────────────────── +include(GNUInstallDirs) +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +install(TARGETS vde EXPORT ViewDesignEngine-targets) +install(EXPORT ViewDesignEngine-targets DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ViewDesignEngine) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1b4bbb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,62 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work. + + 2. Grant of Copyright License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable copyright license to reproduce, prepare + Derivative Works of, publicly display, publicly perform, sublicense, + and distribute the Work and such Derivative Works in Source or + Object form. + + 3. Grant of Patent License. + + Subject to the terms and conditions of this License, each Contributor + hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, + royalty-free, irrevocable patent license. + + 4. Redistribution. + + You may reproduce and distribute copies of the Work or Derivative + Works thereof in any medium, with or without modifications, and in + Source or Object form, provided that You meet the following conditions. + + Copyright 2026 ViewDesignEngine Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..88ebb03 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# ViewDesignEngine — 高性能 CAD 计算几何算法库 + +C++17 实现的计算几何引擎,提供曲线曲面建模、网格处理、布尔运算、空间索引、碰撞检测等底层几何计算能力。 + +## 快速开始 + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j$(nproc) +cd build && ctest --output-on-failure +``` + +## 模块 + +| 模块 | 命名空间 | 说明 | +|------|---------|------| +| foundation | `vde::foundation` | 数学封装、公差系统、精确谓词、I/O | +| core | `vde::core` | 基本几何类型、变换、距离、凸包 | +| curves | `vde::curves` | Bezier / B-Spline / NURBS 曲线曲面 | +| mesh | `vde::mesh` | 半边网格、Delaunay、简化、平滑 | +| spatial | `vde::spatial` | BVH、Octree、KD-Tree、R-Tree | +| boolean | `vde::boolean` | 2D / 3D 布尔运算 | +| collision | `vde::collision` | GJK、SAT、射线相交 | + +## 许可证 + +Apache License 2.0 — 详见 [LICENSE](LICENSE) + +## 文档 + +详见 [`docs/`](docs/) 目录,入口:`docs/00-文档目录.md` diff --git a/cmake/CompilerSettings.cmake b/cmake/CompilerSettings.cmake new file mode 100644 index 0000000..397b633 --- /dev/null +++ b/cmake/CompilerSettings.cmake @@ -0,0 +1,41 @@ +# Compiler settings for ViewDesignEngine +# Include with: include(cmake/CompilerSettings.cmake) + +# ── GCC / Clang ─────────────────────────────────── +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang|AppleClang") + target_compile_options(vde_compile_options INTERFACE + -Wall -Wextra -Wpedantic + -Wshadow -Wnon-virtual-dtor + -Woverloaded-virtual -Wconversion + -Wsign-conversion -Wnull-dereference + -Wdouble-promotion -Wformat=2 + ) + if(ENABLE_SANITIZERS) + target_compile_options(vde_compile_options INTERFACE + -fsanitize=address,undefined -fno-omit-frame-pointer + ) + target_link_options(vde_compile_options INTERFACE + -fsanitize=address,undefined + ) + endif() + if(ENABLE_LTO AND CMAKE_BUILD_TYPE STREQUAL "Release") + target_compile_options(vde_compile_options INTERFACE -flto) + target_link_options(vde_compile_options INTERFACE -flto) + endif() +endif() + +# ── MSVC ─────────────────────────────────────────── +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + target_compile_options(vde_compile_options INTERFACE + /W4 /permissive- /Zc:__cplusplus /utf-8 + ) + target_compile_definitions(vde_compile_options INTERFACE + _CRT_SECURE_NO_WARNINGS NOMINMAX + ) +endif() + +# ── 公共编译定义 ─────────────────────────────────── +target_compile_definitions(vde_compile_options INTERFACE + VDE_VERSION_MAJOR=${PROJECT_VERSION_MAJOR} + VDE_VERSION_MINOR=${PROJECT_VERSION_MINOR} +) diff --git a/cmake/Doxygen.cmake b/cmake/Doxygen.cmake new file mode 100644 index 0000000..4df7f6e --- /dev/null +++ b/cmake/Doxygen.cmake @@ -0,0 +1,18 @@ +find_package(Doxygen QUIET) +if(Doxygen_FOUND) + set(DOXYGEN_PROJECT_NAME "ViewDesignEngine") + set(DOXYGEN_PROJECT_VERSION ${PROJECT_VERSION}) + set(DOXYGEN_INPUT "${CMAKE_SOURCE_DIR}/include") + set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs") + set(DOXYGEN_EXTRACT_ALL YES) + set(DOXYGEN_EXTRACT_PRIVATE YES) + set(DOXYGEN_RECURSIVE YES) + set(DOXYGEN_GENERATE_HTML YES) + set(DOXYGEN_GENERATE_XML YES) + + doxygen_add_docs(doc + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/docs + COMMENT "Generate API documentation" + ) +endif() diff --git a/cmake/ViewDesignEngineConfig.cmake.in b/cmake/ViewDesignEngineConfig.cmake.in new file mode 100644 index 0000000..ceb6408 --- /dev/null +++ b/cmake/ViewDesignEngineConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/ViewDesignEngineTargets.cmake") + +check_required_components(ViewDesignEngine) diff --git a/docs/00-开发计划.md b/docs/00-开发计划.md new file mode 100644 index 0000000..4e552f1 --- /dev/null +++ b/docs/00-开发计划.md @@ -0,0 +1,45 @@ +# 开发计划 + +## 目录 + +1. [版本规划](#1-版本规划) +2. [Sprint 划分](#2-sprint-划分) +3. [里程碑](#3-里程碑) + +--- + +## 1. 版本规划 + +| 版本 | 目标 | 对标 | +|------|------|------| +| **v0.1.0** | 核心几何 + 曲线曲面 + 基础网格 | CGAL Kernel + NURBS + Clipper2 | +| **v0.5.0** | 完整网格处理 + 空间索引 + 碰撞检测 | CGAL Mesh + Manifold | +| **v1.0.0** | 完整布尔运算 + 稳定 API + 全覆盖测试 | 中等规模几何引擎 | +| **v2.0.0** | B-Rep + STEP 导入 + 工业级健壮性 | OCCT 轻量替代 | + +--- + +## 2. Sprint 划分 + +| Sprint | 模块 | 内容 | 状态 | +|--------|------|------|------| +| **S1** | 工程骨架 | 目录结构、CMake 构建、开发环境 | ✅ 完成 | +| **S2** | foundation + core | 数学类型、公差、基本几何、变换、凸包 | ✅ 完成 | +| **S3** | curves | Bezier、B-Spline、NURBS 曲线与曲面 | ✅ 完成 | +| **S4** | mesh | 半边网格、Delaunay 2D | ✅ 核心完成 | +| **S5** | spatial + collision | BVH、GJK、射线相交 | ✅ 核心完成 | +| **S6** | boolean | 2D 布尔运算 | ✅ 基础完成 | +| **S7** | 测试 + 示例 | 11 个测试文件、6 个示例 | ✅ 完成 | +| **S8** | 补齐实现 | mesh simplify/smooth/boolean、Octree/KDTree/RTree | 🔜 待开发 | +| **S9** | 3D 扩展 | 3D Delaunay、3D 布尔运算、B-Rep 支持 | 📋 规划中 | + +--- + +## 3. 里程碑 + +| 里程碑 | 时间 | 交付物 | +|--------|------|--------| +| **M1: MVP** | ✅ 已完成 | 可编译工程 + 40 个头文件 + 核心实现 | +| **M2: 功能完整** | 🔜 | 所有模块实现完整、测试覆盖率 ≥ 80% | +| **M3: 性能达标** | 📋 | 通过性能基准测试 | +| **M4: v1.0 发布** | 📋 | 稳定 API + 完整文档 + 示例教程 | diff --git a/docs/00-文档目录.md b/docs/00-文档目录.md new file mode 100644 index 0000000..a73bb06 --- /dev/null +++ b/docs/00-文档目录.md @@ -0,0 +1,35 @@ +# ViewDesignEngine 工程文档 + +## 文档索引 + +| 编号 | 文档 | 说明 | +|------|------|------| +| **第一阶段:需求分析** | | | +| 01 | [市场调研与竞品分析](01-需求分析/01-市场调研与竞品分析.md) | CGAL、OCCT、libigl 等 7 个引擎对标 | +| 02 | [需求规格说明书](01-需求分析/02-需求规格说明书.md) | 10 类功能需求 + 5 类非功能需求 | +| 03 | [用例文档](01-需求分析/03-用例文档.md) | 6 个核心用例场景 | +| **第二阶段:概要设计** | | | +| 04 | [系统架构设计](02-概要设计/01-系统架构设计.md) | 7 层模块架构 + 数据流 + 设计原则 | +| 05 | [模块划分](02-概要设计/02-模块划分.md) | 7 个模块详细组件清单与依赖关系 | +| 06 | [接口定义](02-概要设计/03-接口定义.md) | C++ 虚接口抽象 + C API + 错误处理约定 | +| **第三阶段:详细设计** | | | +| 07 | [数据结构设计](03-详细设计/01-数据结构设计.md) | 半边网格、NURBS、BVH、公差系统 | +| 08 | [核心算法设计](03-详细设计/02-核心算法设计.md) | Delaunay、GJK、QEM、SAH 等算法伪代码 | +| 09 | [类设计](03-详细设计/03-类设计.md) | 类关系图、工厂模式、策略模式、访问者模式 | +| **第四阶段:编码实现** | | | +| 10 | [编码规范](04-编码实现/01-编码规范.md) | C++17 命名、格式、注释、内存、错误处理规范 | +| 11 | [目录结构](04-编码实现/02-目录结构.md) | 完整工程目录树 + CMakeLists.txt 骨架 | +| **第五阶段:测试验证** | | | +| 12 | [测试计划](05-测试验证/01-测试计划.md) | 测试策略、CI 流程、性能基准、回归测试 | +| 13 | [测试用例](05-测试验证/02-测试用例.md) | 8 组详细测试用例,含输入/预期输出 | +| **第六阶段:部署维护** | | | +| 14 | [构建指南](06-部署维护/01-构建指南.md) | Linux/Win/macOS 构建 + FetchContent 集成 | +| 15 | [API 参考](06-部署维护/02-API参考.md) | 7 个模块全部公开 API 签名 | + +## 技术栈 + +- **语言**: C++17 +- **构建**: CMake 3.16+ +- **依赖**: Eigen 3.3+(唯一必需依赖) +- **测试**: Google Test +- **许可证**: Apache 2.0 diff --git a/docs/01-需求分析/01-市场调研与竞品分析.md b/docs/01-需求分析/01-市场调研与竞品分析.md new file mode 100644 index 0000000..ade7ee0 --- /dev/null +++ b/docs/01-需求分析/01-市场调研与竞品分析.md @@ -0,0 +1,282 @@ +# 市场调研与竞品分析 + +## 目录 + +1. [主流几何引擎概览](#1-主流几何引擎概览) + - 1.1 CGAL + - 1.2 Open CASCADE (OCCT) + - 1.3 libigl + - 1.4 Clipper2 + - 1.5 Manifold + - 1.6 GEOS + - 1.7 其他重要竞品 +2. [功能矩阵对比](#2-功能矩阵对比) +3. [市场空白与定位](#3-市场空白与定位) +4. [对标策略](#4-对标策略) +5. [结论](#5-结论) + +--- + +## 1. 主流几何引擎概览 + +### 1.1 CGAL — 计算几何算法库 + +| 维度 | 详情 | +|------|------| +| **定位** | 学术界黄金标准,最全面的计算几何算法库 | +| **语言** | C++(核心)+ Python 绑定 | +| **许可证** | GPLv3+ / 商业双许可 | +| **历史** | 1996 年至今,28 年 | +| **规模** | 160+ 个 Package,数百万行代码 | +| **依赖** | Boost, GMP/MPFR | +| **维护方** | GeometryFactory(法国)+ 学术社区 | + +**核心模块**: +- 2D/3D 几何内核(笛卡尔坐标、齐次坐标、球面坐标) +- 2D 排列(Arrangement)、多边形布尔运算 +- 3D 多面体、Nef 多面体(精确布尔) +- 网格生成(2D/3D Delaunay、曲面网格) +- 网格处理(简化、重网格化、孔洞填充) +- AABB Tree / KD-Tree 空间索引 +- NURBS 曲线曲面 +- 凸包、Voronoi 图、Alpha Shapes +- 点集处理(配准、重建、分类) + +**优势**:算法最全、精确计算(GMP)、学术严谨 +**劣势**:GPL 许可证限制商业闭源;API 模板重、编译极慢;学习曲线陡峭 + +--- + +### 1.2 Open CASCADE Technology (OCCT) + +| 维度 | 详情 | +|------|------| +| **定位** | 工业级 CAD 建模内核,全球唯一开源工业 CAD 内核 | +| **语言** | C++ | +| **许可证** | LGPL 2.1 | +| **历史** | 1990 年代至今,30+ 年 | +| **规模** | 数千万行代码 | +| **维护方** | Open Cascade SAS(法国) | + +**核心模块**: +- **基础类**:数学工具、内存管理、异常处理 +- **建模数据**:几何体(点/线/面/体)、拓扑(B-Rep) +- **建模算法**:布尔运算、倒角、抽壳、扫掠、放样 +- **可视化**:OpenGL 渲染 +- **数据交换**:STEP, IGES, STL, VRML, glTF, OBJ, XDE +- **应用框架**:文档管理、撤销/重做 + +**优势**:完整 B-Rep 支持、工业级健壮性、STEP/IGES 数据交换、LGPL 友好 +**劣势**:极其庞大(数百万行)、API 老旧(非现代 C++)、编译困难 + +--- + +### 1.3 libigl — 轻量几何处理库 + +| 维度 | 详情 | +|------|------| +| **定位** | 学术研究用轻量网格处理库 | +| **语言** | C++(以 Header-only 为主)+ Python 绑定 | +| **许可证** | MPL 2.0(商业友好) | +| **历史** | 2013 年至今 | +| **维护方** | Alec Jacobson(多伦多大学)+ 社区 | + +**核心功能**: +- 网格读写(OBJ, STL, PLY, OFF 等) +- 网格布尔运算(依赖 CGAL 或 Cork) +- 网格简化、平滑、参数化 +- 离散微分几何(曲率、测地线、Laplacian) +- 变形(ARAP、双调和、LBS) +- 体素化、Marching Cubes +- 四面体化(依赖 TetGen) +- 可视化(基于 ImGui) + +**优势**:代码简洁、Header-only、教程丰富、MPL 许可证 +**劣势**:功能深度不足、强依赖外部库、不适用工业级精度 + +--- + +### 1.4 Clipper2 — 2D 多边形裁剪 + +| 维度 | 详情 | +|------|------| +| **定位** | 2D 多边形布尔运算的事实标准 | +| **语言** | C++, C#, Delphi, Python 绑定 | +| **许可证** | Boost Software License | +| **算法** | Vatti 扫描线算法 | + +**核心功能**: +- 多边形布尔运算(并集、交集、差集、异或) +- 多边形偏移(膨胀/腐蚀) +- 支持任意多边形(含孔洞、自交) +- 整数坐标(64-bit),无浮点精度问题 + +**优势**:极快、极其健壮、许可证最自由 +**劣势**:仅 2D、无 3D 能力 + +--- + +### 1.5 Manifold — 3D 网格布尔 + +| 维度 | 详情 | +|------|------| +| **定位** | 最快的 3D 网格布尔运算库 | +| **语言** | C++(C 绑定),WASM/JS/Python 绑定 | +| **许可证** | Apache 2.0 | +| **算法** | 基于精确算术 + 鲁棒拓扑 | + +**核心功能**: +- 网格布尔运算(并集、交集、差集) +- 网格修复(水密化) +- 体素化 +- 极高性能(比 CGAL/Cork 快 10-100 倍) + +**优势**:极快、鲁棒、Apache 2.0 许可证 +**劣势**:功能单一(仅网格布尔)、无曲线曲面 + +--- + +### 1.6 GEOS — 开源几何引擎 + +| 维度 | 详情 | +|------|------| +| **定位** | GIS 领域的 2D 几何引擎 | +| **语言** | C++(核心)+ C API | +| **许可证** | LGPL | +| **应用** | PostGIS、QGIS、Shapely 的底层引擎 | + +**核心功能**: +- OGC 简单要素标准实现 +- 2D 空间谓词(包含、相交、相切等) +- 空间操作(缓冲区、并集、交集、凸包) +- WKT/WKB 格式 +- 空间索引(STRtree) + +**优势**:GIS 标准、生态成熟、久经考验 +**劣势**:仅 2D、无 CAD 建模能力 + +--- + +### 1.7 其他重要竞品 + +| 库 | 定位 | 许可证 | 关键功能 | +|----|------|--------|---------| +| **Eigen** | C++ 线性代数 | MPL 2.0 | 矩阵/向量运算、几何变换、数值求解 | +| **Cork** | 网格布尔 | LGPL | 3D 网格布尔运算 | +| **Carve** | CSG 引擎 | GPLv2 | 快速 CSG 布尔运算 | +| **TetGen** | 四面体网格生成 | AGPLv3 | 约束 Delaunay 四面体化 | +| **Triangle** | 2D 三角剖分 | 自由(非商业) | 约束 Delaunay 三角剖分 | +| **PCL** | 点云处理 | BSD | 点云滤波、配准、分割、重建 | +| **VTK** | 可视化 | BSD | 科学可视化、网格处理 | + +--- + +## 2. 功能矩阵对比 + +| 功能领域 | CGAL | OCCT | libigl | Clipper2 | Manifold | GEOS | **VDE 目标** | +|---------|------|------|--------|----------|----------|------|------------| +| 基本几何类型 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | +| 2D 布尔运算 | ✅ | ✅ | ❌ | ✅🏆 | ❌ | ✅ | ✅ | +| 3D 网格布尔 | ✅ | ✅ | 🔗CGAL | ❌ | ✅🏆 | ❌ | ✅ | +| B-Rep 布尔 | ❌ | ✅🏆 | ❌ | ❌ | ❌ | ❌ | 🔜 | +| NURBS 曲线 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| NURBS 曲面 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| 网格简化 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | +| 网格平滑 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | +| Delaunay 2D | ✅🏆 | ✅ | 🔗 | ❌ | ❌ | ✅ | ✅ | +| 凸包 2D/3D | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | +| BVH 空间索引 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| GJK 碰撞检测 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| 射线相交 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | +| STEP 导入 | ❌ | ✅🏆 | ❌ | ❌ | ❌ | ❌ | 🔜 | +| 精确谓词 | ✅🏆 | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | +| 现代 C++ (17+) | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ✅🏆 | +| 许可证 | GPL/商 | LGPL | MPL | Boost | Apache | LGPL | **Apache 2.0** | + +> 图例:✅ 支持 🔗 依赖外部 🔜 规划中 ❌ 不支持 🏆 最强 + +--- + +## 3. 市场空白与定位 + +### 3.1 现有引擎的痛点 + +| 痛点 | 说明 | +|------|------| +| **CGAL 许可证** | GPLv3 限制商业闭源使用,商业许可昂贵(€10k+) | +| **CGAL 编译** | Boost 依赖 + 模板膨胀 → 编译时间以分钟计 | +| **OCCT 体积** | 数百万行代码,学习周期以月计 | +| **OCCT API** | 非现代 C++,API 设计陈旧 | +| **libigl 深度** | 功能浅,依赖链复杂,缺乏自身核心算法 | +| **各库割裂** | 需要曲线 + 需要网格布尔 + 需要碰撞检测 → 分别引入三个库 | + +### 3.2 差异化定位 + +``` + ┌─ 工业深度 ─┐ + │ OCCT │ + │ │ + 算法广度 ←── CGAL ──→ │ ★ VDE ★ │ ←── 现代 C++ + │ │ Apache 2.0 + │ Manifold │ + └────────────┘ + Clipper2 | libigl +``` + +VDE 的核心特征: +1. **Apache 2.0 许可证** — 商业友好,无 GPL 传染 +2. **现代 C++17** — 清晰 API,零 Boost 依赖(仅 Eigen) +3. **一站式覆盖** — CAD 建模 + 网格处理 + 碰撞检测在同一库中 +4. **编译友好** — 数分钟级全量编译,非小时级 +5. **中等规模** — 目标 10-20 万行,不像 CGAL/OCCT 百万行级别 + +### 3.3 目标用户 + +- 自研 CAD/CAM 软件,需要底层几何内核但不想被 GPL 限制 +- 机器人/自动驾驶仿真,需要碰撞检测 + 网格处理 +- 3D 打印切片软件,需要网格布尔 + 修复 +- 游戏引擎编辑器,需要几何操作 + +--- + +## 4. 对标策略 + +### 4.1 V1.0 目标(当前) + +对标 **CGAL 核心 Package + Clipper2 + Manifold 三者交集**: + +| 功能 | 对标库 | 当前状态 | +|------|--------|---------| +| 基本几何 + 变换 | CGAL Kernel | ✅ 已实现 | +| 精确谓词 | CGAL Exact Predicates | ✅ 基础版 | +| 2D Delaunay | CGAL 2D Triangulation | ✅ 已实现 | +| 2D 布尔 | Clipper2 | ✅ S-H 裁剪 | +| BVH 空间索引 | CGAL AABB Tree | ✅ SAH 构建 | +| GJK 碰撞检测 | — | ✅ 已实现 | +| NURBS 曲线 | CGAL NURBS | ✅ 已实现 | +| 半边网格 | CGAL Polyhedron | ✅ 已实现 | + +### 4.2 V1.5 规划 + +| 功能 | 对标库 | 实现方法 | +|------|--------|---------| +| 网格布尔 | Manifold | 自研精确拓扑算法 | +| QEM 网格简化 | CGAL Surface Mesh Simplification | 自研 | +| 3D Delaunay | CGAL 3D Triangulation | 自研 | +| Laplacian 平滑 | libigl | 自研 | + +### 4.3 V2.0 愿景 + +| 功能 | 对标库 | +|------|--------| +| B-Rep 布尔 | OCCT Modeling Algorithms | +| 倒角/抽壳 | OCCT | +| STEP 导入 | OCCT Data Exchange | +| 参数化 | libigl | +| 四面体化 | TetGen | + +--- + +## 5. 结论 + +当前 CAD 几何引擎市场呈现"两极分化":一端是 CGAL(学术全面但 GPL)和 OCCT(工业强大但笨重),另一端是各垂直工具(Clipper2、Manifold 等)。**缺少一个 Apache 2.0 许可证、现代 C++、一站式覆盖 CAD+Mesh+Collision 的中等规模几何引擎**——这正是 ViewDesignEngine 的市场定位。 diff --git a/docs/01-需求分析/02-需求规格说明书.md b/docs/01-需求分析/02-需求规格说明书.md new file mode 100644 index 0000000..98375cf --- /dev/null +++ b/docs/01-需求分析/02-需求规格说明书.md @@ -0,0 +1,170 @@ +# 需求规格说明书 + +## 目录 + +1. [引言](#1-引言) +2. [功能需求](#2-功能需求) +3. [非功能需求](#3-非功能需求) + +--- + +## 1. 引言 + +### 1.1 目的 + +本文档定义 ViewDesignEngine 几何算法引擎的功能需求与非功能需求,作为后续设计、实现与测试的基线。 + +### 1.2 范围 + +本引擎提供底层几何计算能力,覆盖二维和三维空间的基础几何运算、曲线曲面建模、网格处理、布尔运算、空间索引和碰撞检测。 + +### 1.3 术语定义 + +| 术语 | 定义 | +|------|------| +| CSG | 构造实体几何(Constructive Solid Geometry) | +| NURBS | 非均匀有理 B 样条(Non-Uniform Rational B-Spline) | +| BVH | 包围体层次结构(Bounding Volume Hierarchy) | +| GJK | Gilbert-Johnson-Keerthi 碰撞检测算法 | +| Delaunay | Delaunay 三角剖分 | +| B-Rep | 边界表示法(Boundary Representation) | +| QEM | 二次误差度量(Quadric Error Metrics) | + +--- + +## 2. 功能需求 + +### FR-01:基本几何类型 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-01.1 | Point2D / Point3D:二维/三维点,支持算术运算 | P0 | +| FR-01.2 | Vector2D / Vector3D:向量运算(加减、点积、叉积、归一化) | P0 | +| FR-01.3 | Line2D / Line3D / Ray / Segment:直线、射线、线段 | P0 | +| FR-01.4 | Plane:平面定义与运算 | P0 | +| FR-01.5 | Triangle / Polygon:三角形与多边形(法向量、面积、点包含判断) | P0 | +| FR-01.6 | BoundingBox:轴对齐包围盒(AABB)与定向包围盒(OBB) | P0 | + +### FR-02:几何变换 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-02.1 | 平移、旋转、缩放 | P0 | +| FR-02.2 | 变换矩阵(4×4 齐次坐标) | P0 | +| FR-02.3 | 组合变换(变换链) | P1 | +| FR-02.4 | 反射、剪切、投影 | P2 | + +### FR-03:曲线建模 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-03.1 | Bezier 曲线(任意阶) | P0 | +| FR-03.2 | B-Spline 曲线 | P0 | +| FR-03.3 | NURBS 曲线 | P0 | +| FR-03.4 | 曲线求值(de Casteljau / Cox-de Boor 算法) | P0 | +| FR-03.5 | 曲线求导(一阶/二阶导数) | P1 | +| FR-03.6 | 曲线分割与拼接 | P1 | +| FR-03.7 | 弧长参数化 | P2 | + +### FR-04:曲面建模 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-04.1 | Bezier 曲面 | P1 | +| FR-04.2 | B-Spline 曲面 | P1 | +| FR-04.3 | NURBS 曲面 | P1 | +| FR-04.4 | 曲面求值与求导 | P1 | +| FR-04.5 | 曲面离散化(三角网格化) | P1 | + +### FR-05:网格处理 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-05.1 | 半边数据结构(Half-Edge) | P0 | +| FR-05.2 | Delaunay 三角剖分(2D) | P0 | +| FR-05.3 | 约束 Delaunay 三角剖分(2D) | P2 | +| FR-05.4 | 网格简化(QEM 算法) | P1 | +| FR-05.5 | 网格平滑(Laplacian / Taubin) | P1 | +| FR-05.6 | 网格布尔运算(并、交、差) | P1 | +| FR-05.7 | 网格质量评估 | P1 | +| FR-05.8 | 网格修复(孔洞填充、非流形修复) | P2 | + +### FR-06:布尔运算(CSG) + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-06.1 | 二维多边形布尔运算(并、交、差、异或) | P0 | +| FR-06.2 | 三维网格布尔运算 | P1 | +| FR-06.3 | B-Rep 实体布尔运算 | P2 | + +### FR-07:空间索引 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-07.1 | 包围体层次结构(BVH) | P0 | +| FR-07.2 | 八叉树(Octree) | P1 | +| FR-07.3 | KD-Tree | P1 | +| FR-07.4 | R-Tree | P2 | + +### FR-08:碰撞检测 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-08.1 | GJK 算法(凸体距离/碰撞检测) | P0 | +| FR-08.2 | SAT 分离轴定理(多面体) | P1 | +| FR-08.3 | 射线-三角形相交(Möller-Trumbore) | P0 | +| FR-08.4 | 三角形-三角形相交 | P1 | +| FR-08.5 | AABB / OBB 相交测试 | P1 | + +### FR-09:几何计算 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-09.1 | 距离计算(点-点、点-线、点-面、线-线) | P0 | +| FR-09.2 | 面积计算(多边形、曲面) | P1 | +| FR-09.3 | 最近点查询 | P1 | +| FR-09.4 | 凸包计算(2D / 3D) | P0 | + +### FR-10:公差与精度 + +| 编号 | 需求 | 优先级 | +|------|------|--------| +| FR-10.1 | 可配置的几何公差 | P0 | +| FR-10.2 | 自适应精度策略 | P1 | +| FR-10.3 | 鲁棒的浮点谓词(Shewchuk 自适应精度) | P1 | + +--- + +## 3. 非功能需求 + +### NFR-01:性能 + +| 指标 | 输入规模 | 目标 | +|------|---------|------| +| 布尔运算 | 百万面级网格 | ≤ 30 秒 | +| 最近点查询 | 10 万三角面(BVH) | ≤ 1 毫秒 | +| Delaunay 剖分 | 百万点 | ≤ 10 秒 | + +### NFR-02:可靠性 + +- 核心算法单元测试覆盖率 ≥ 90% +- 回归测试套件覆盖所有公开 API +- 边界条件与退化情况的鲁棒处理 + +### NFR-03:可移植性 + +- 支持 Linux(GCC 9+ / Clang 12+) +- 支持 Windows(MSVC 2019+) +- 支持 macOS(Clang 12+) + +### NFR-04:可扩展性 + +- 模块化架构,各模块独立编译 +- 清晰的接口抽象层,支持算法替换 +- 插件式空间索引后端 + +### NFR-05:可用性 + +- 完善的 API 文档(Doxygen) +- 丰富的示例代码 +- 入门指南与教程 diff --git a/docs/01-需求分析/03-用例文档.md b/docs/01-需求分析/03-用例文档.md new file mode 100644 index 0000000..bcd986b --- /dev/null +++ b/docs/01-需求分析/03-用例文档.md @@ -0,0 +1,106 @@ +# 用例文档 + +## 目录 + +1. [UC-01:创建几何实体](#uc-01创建几何实体) +2. [UC-02:曲线曲面建模](#uc-02曲线曲面建模) +3. [UC-03:网格处理与分析](#uc-03网格处理与分析) +4. [UC-04:CSG 布尔运算](#uc-04csg-布尔运算) +5. [UC-05:碰撞检测](#uc-05碰撞检测) +6. [UC-06:空间查询](#uc-06空间查询) + +--- + +## UC-01:创建几何实体 + +**参与者**:CAD 应用开发者 + +**前置条件**:引擎已初始化 + +**主流程**: +1. 开发者创建基本几何实体(点、线、面) +2. 对实体施加几何变换 +3. 组合多个实体形成复杂几何 + +**后置条件**:几何实体可被后续运算引用 + +--- + +## UC-02:曲线曲面建模 + +**参与者**:建模工程师 + +**前置条件**:引擎已初始化 + +**主流程**: +1. 定义控制点和节点向量 +2. 创建 NURBS 曲线或曲面 +3. 对曲线/曲面进行求值、求导 +4. 将曲面离散化为三角网格用于渲染 + +**后置条件**:曲线/曲面数据可导出为标准格式 + +--- + +## UC-03:网格处理与分析 + +**参与者**:CAE 预处理工程师 + +**前置条件**:已导入或生成初始网格 + +**主流程**: +1. 加载网格数据(OBJ/STL/PLY 格式) +2. 评估网格质量 +3. 执行网格简化或平滑操作 +4. 进行 Delaunay 三角剖分优化 +5. 导出处理后的网格 + +**后置条件**:优化后的网格可用于有限元分析 + +--- + +## UC-04:CSG 布尔运算 + +**参与者**:参数化建模系统 + +**前置条件**:存在两个或多个实体 + +**主流程**: +1. 定义两个三维实体 +2. 执行并集、交集或差集运算 +3. 获取结果实体的网格表示 +4. 将结果用于后续建模操作 + +**后置条件**:结果实体可继续参与建模操作 + +--- + +## UC-05:碰撞检测 + +**参与者**:CAM 刀路规划模块、机器人仿真 + +**前置条件**:已加载刀具模型和工件模型 + +**主流程**: +1. 构建 BVH 加速结构 +2. 执行刀具与工件之间的碰撞检测 +3. 计算最小分离距离 +4. 返回碰撞点与穿透深度 + +**后置条件**:碰撞结果用于刀路修正或路径规划 + +--- + +## UC-06:空间查询 + +**参与者**:交互式选择工具 + +**前置条件**:场景中包含大量几何实体 + +**主流程**: +1. 构建八叉树或 KD-Tree 空间索引 +2. 执行范围查询(框选实体) +3. 执行最近点查询(鼠标吸附) +4. 返回查询结果 + +**后置条件**:选中的实体高亮显示 diff --git a/docs/02-概要设计/01-系统架构设计.md b/docs/02-概要设计/01-系统架构设计.md new file mode 100644 index 0000000..80bda46 --- /dev/null +++ b/docs/02-概要设计/01-系统架构设计.md @@ -0,0 +1,87 @@ +# 系统架构设计 + +## 目录 + +1. [整体架构](#1-整体架构) +2. [模块层次](#2-模块层次) +3. [数据流](#3-数据流) +4. [设计原则](#4-设计原则) + +--- + +## 1. 整体架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 公开 API 层 │ +│ (C++ 头文件,稳定 ABI,Doxygen 文档) │ +├─────────────────────────────────────────────────────────┤ +│ ┌─────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ +│ │ 几何 │ │ 曲线曲面 │ │ 网格 │ │ 空间索引 │ │ +│ │ 核心 │ │ 建模 │ │ 处理 │ │ │ │ +│ └────┬────┘ └────┬─────┘ └───┬────┘ └──────┬───────┘ │ +│ │ │ │ │ │ +│ ┌────┴───────────┴───────────┴──────────────┴───────┐ │ +│ │ CSG 布尔引擎 │ │ +│ └────────────────────────┬──────────────────────────┘ │ +│ ┌────────────────────────┴──────────────────────────┐ │ +│ │ 碰撞检测层 │ │ +│ └────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────┤ +│ 基础设施层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 数学库 │ │ 公差系统 │ │ I/O │ │ 内存管理 │ │ +│ │ (Eigen) │ │ │ │(OBJ/STL) │ │ │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 模块层次 + +| 层级 | 模块 | 命名空间 | 职责 | +|------|------|---------|------| +| L0 基础设施 | foundation | `vde::foundation` | 数学库封装、精确谓词、内存管理、I/O | +| L1 几何核心 | core | `vde::core` | 基本几何类型、变换、距离计算、凸包 | +| L2 曲线曲面 | curves | `vde::curves` | Bezier/B-Spline/NURBS 曲线与曲面 | +| L3 网格处理 | mesh | `vde::mesh` | 网格数据结构、剖分、简化、平滑 | +| L4 空间索引 | spatial | `vde::spatial` | BVH、Octree、KD-Tree、R-Tree | +| L5 布尔运算 | boolean | `vde::boolean` | 2D/3D CSG 布尔运算 | +| L5 碰撞检测 | collision | `vde::collision` | GJK、SAT、射线相交 | + +**依赖规则**: +- 上层可依赖下层,下层不依赖上层 +- 同层模块间通过接口抽象解耦 +- L5 层(布尔运算/碰撞检测)依赖 L1-L4 + +--- + +## 3. 数据流 + +``` +输入数据 (OBJ/STL/STEP) + │ + ▼ +[ I/O 解析 ] ──► 几何核心(基本实体) + │ + ▼ +[ 曲线曲面建模 ] ──► 离散化 ──► [ 网格处理 ] + │ │ + ▼ ▼ +[ CSG 布尔 ] ◄──────────── [ 空间索引 ] + │ + ▼ +输出数据 (OBJ/STL/B-Rep) +``` + +--- + +## 4. 设计原则 + +1. **零开销抽象**:模板与编译期多态,不引入不必要的虚函数开销 +2. **数据驱动**:使用 SoA(数组结构体)布局优化缓存命中率 +3. **不可变优先**:几何实体默认不可变,变换操作返回新实体 +4. **RAII 资源管理**:所有资源由智能指针管理 +5. **异常安全**:提供基本保证以上,核心路径提供强保证 +6. **单一依赖**:仅依赖 Eigen,不引入 Boost 等重型库 diff --git a/docs/02-概要设计/02-模块划分.md b/docs/02-概要设计/02-模块划分.md new file mode 100644 index 0000000..065d7cb --- /dev/null +++ b/docs/02-概要设计/02-模块划分.md @@ -0,0 +1,135 @@ +# 模块划分 + +## 目录 + +1. [M01:基础设施层(foundation)](#m01基础设施层foundation) +2. [M02:几何核心层(core)](#m02几何核心层core) +3. [M03:曲线曲面层(curves)](#m03曲线曲面层curves) +4. [M04:网格处理层(mesh)](#m04网格处理层mesh) +5. [M05:空间索引层(spatial)](#m05空间索引层spatial) +6. [M06:布尔运算层(boolean)](#m06布尔运算层boolean) +7. [M07:碰撞检测层(collision)](#m07碰撞检测层collision) +8. [构建目标总览](#8-构建目标总览) + +--- + +## M01:基础设施层(foundation) + +**路径**:`src/foundation/` +**命名空间**:`vde::foundation` + +| 组件 | 文件 | 说明 | +|------|------|------| +| 数学封装 | `math_types.h/cpp` | Eigen 二次封装,统一向量/矩阵接口 | +| 精确谓词 | `exact_predicates.h/cpp` | Shewchuk 自适应精度谓词 | +| 公差系统 | `tolerance.h/cpp` | 全局/局部公差管理 | +| 内存池 | `memory_pool.h/cpp` | 高性能内存分配器 | +| I/O | `io_obj.h/cpp`, `io_stl.h/cpp` | OBJ/STL 文件读写 | +| 错误码 | `error_codes.h` | 统一错误码枚举 | + +**公开头文件**:`include/vde/foundation/*.h` + +--- + +## M02:几何核心层(core) + +**路径**:`src/core/` +**命名空间**:`vde::core` + +| 组件 | 文件 | 说明 | +|------|------|------| +| 基本类型 | `point.h`, `line.h`, `plane.h` | 点/线/面的类型别名与便捷函数 | +| 基本图形 | `triangle.h`, `polygon.h` | 三角形与多边形 | +| 包围盒 | `aabb.h` | 轴对齐包围盒 | +| 变换 | `transform.h` | 平移/旋转/缩放的便捷函数 | +| 距离计算 | `distance.h` | 各类几何距离计算 | +| 凸包 | `convex_hull.h` | 2D 与 3D 凸包算法 | + +--- + +## M03:曲线曲面层(curves) + +**路径**:`src/curves/` +**命名空间**:`vde::curves` + +| 组件 | 文件 | 说明 | +|------|------|------| +| Bezier 曲线 | `bezier_curve.h/cpp` | 任意阶 Bezier 曲线 | +| B-Spline 曲线 | `bspline_curve.h/cpp` | B-Spline 曲线 | +| NURBS 曲线 | `nurbs_curve.h/cpp` | NURBS 曲线 | +| Bezier 曲面 | `bezier_surface.h/cpp` | 张量积 Bezier 曲面 | +| B-Spline 曲面 | `bspline_surface.h/cpp` | B-Spline 曲面 | +| 离散化 | `tessellation.h/cpp` | 曲线/曲面三角网格化 | + +--- + +## M04:网格处理层(mesh) + +**路径**:`src/mesh/` +**命名空间**:`vde::mesh` + +| 组件 | 文件 | 说明 | +|------|------|------| +| 半边网格 | `halfedge_mesh.h/cpp` | 半边数据结构与拓扑查询 | +| Delaunay 剖分 | `delaunay_2d.h/cpp` | 2D Bowyer-Watson 算法 | +| 网格简化 | `mesh_simplify.h/cpp` | QEM 算法 | +| 网格平滑 | `mesh_smooth.h/cpp` | Laplacian / Taubin 平滑 | +| 网格布尔 | `mesh_boolean.h/cpp` | 3D 网格布尔运算 | +| 质量评估 | `mesh_quality.h/cpp` | 网格质量指标 | +| 网格修复 | `mesh_repair.h/cpp` | 孔洞填充、非流形修复 | + +--- + +## M05:空间索引层(spatial) + +**路径**:`src/spatial/` +**命名空间**:`vde::spatial` + +| 组件 | 文件 | 说明 | +|------|------|------| +| 索引接口 | `spatial_index.h` | 空间索引抽象接口(虚基类) | +| BVH | `bvh.h/cpp` | 包围体层次结构 | +| 八叉树 | `octree.h/cpp` | 八叉树空间划分 | +| KD-Tree | `kd_tree.h/cpp` | K 维树 | +| R-Tree | `r_tree.h/cpp` | R 树 | + +--- + +## M06:布尔运算层(boolean) + +**路径**:`src/boolean/` +**命名空间**:`vde::boolean` + +| 组件 | 文件 | 说明 | +|------|------|------| +| 2D 布尔 | `boolean_2d.h/cpp` | 多边形布尔运算 | +| 网格布尔 | `boolean_mesh.h/cpp` | 三角网格布尔运算 | + +--- + +## M07:碰撞检测层(collision) + +**路径**:`src/collision/` +**命名空间**:`vde::collision` + +| 组件 | 文件 | 说明 | +|------|------|------| +| GJK | `gjk.h/cpp` | GJK 算法(凸体碰撞检测) | +| SAT | `sat.h/cpp` | 分离轴定理(多面体) | +| 射线相交 | `ray_intersect.h/cpp` | 射线-三角形等相交测试 | +| 三角形相交 | `tri_intersect.h/cpp` | 三角形-三角形 Möller 算法 | + +--- + +## 8. 构建目标总览 + +| 目标 | 类型 | 依赖 | +|------|------|------| +| `vde_foundation` | 静态库 | Eigen3 | +| `vde_core` | 静态库 | vde_foundation | +| `vde_curves` | 静态库 | vde_core | +| `vde_mesh` | 静态库 | vde_core | +| `vde_spatial` | 静态库 | vde_core | +| `vde_boolean` | 静态库 | vde_mesh, vde_spatial | +| `vde_collision` | 静态库 | vde_core, vde_spatial | +| `vde` | 聚合库 | 以上全部 | diff --git a/docs/02-概要设计/03-接口定义.md b/docs/02-概要设计/03-接口定义.md new file mode 100644 index 0000000..eb0f620 --- /dev/null +++ b/docs/02-概要设计/03-接口定义.md @@ -0,0 +1,148 @@ +# 接口定义 + +## 目录 + +1. [命名约定](#1-命名约定) +2. [核心接口抽象](#2-核心接口抽象) + - 2.1 空间索引接口 + - 2.2 曲线求值接口 + - 2.3 网格数据结构接口 + - 2.4 布尔运算接口 +3. [稳定 C API(可选)](#3-稳定-c-api可选) +4. [错误处理约定](#4-错误处理约定) + +--- + +## 1. 命名约定 + +| 元素 | 风格 | 示例 | +|------|------|------| +| 命名空间 | snake_case | `vde::core` | +| 类/结构体 | PascalCase | `HalfedgeMesh` | +| 函数/方法 | snake_case | `compute_convex_hull()` | +| 变量 | snake_case | `vertex_count` | +| 成员变量 | snake_case_ | `vertices_` | +| 常量 | UPPER_SNAKE_CASE | `MAX_ITERATIONS` | +| 枚举值 | kPascalCase | `kSuccess` | +| 宏前缀 | VDE_ | `VDE_VERSION_MAJOR` | + +--- + +## 2. 核心接口抽象 + +### 2.1 空间索引接口 + +```cpp +namespace vde::spatial { + +template +class SpatialIndex { +public: + virtual ~SpatialIndex() = default; + virtual void build(const std::vector& items) = 0; + virtual void insert(const T& item) = 0; + virtual bool remove(const T& item) = 0; + virtual std::vector query_range(const AABB3D& range) const = 0; + virtual std::vector query_knn(const Point3D& point, size_t k) const = 0; + virtual std::vector query_ray(const Ray3Dd& ray) const = 0; + virtual void clear() = 0; + virtual size_t size() const = 0; +}; + +} +``` + +### 2.2 曲线求值接口 + +```cpp +namespace vde::curves { + +template +class CurveBase { +public: + virtual ~CurveBase() = default; + virtual int degree() const = 0; + virtual std::pair domain() const = 0; + virtual Point evaluate(double t) const = 0; + virtual Vector derivative(double t, int order = 1) const = 0; + virtual const std::vector>& control_points() const = 0; +}; + +} +``` + +### 2.3 网格数据结构接口 + +```cpp +namespace vde::mesh { + +class MeshBase { +public: + virtual ~MeshBase() = default; + virtual size_t num_vertices() const = 0; + virtual size_t num_faces() const = 0; + virtual const Point3D& vertex(size_t idx) const = 0; + virtual std::vector vertex_faces(size_t idx) const = 0; + virtual Vector3D face_normal(size_t idx) const = 0; + virtual Vector3D vertex_normal(size_t idx) const = 0; + virtual bool is_boundary_vertex(size_t idx) const = 0; + virtual void update_normals() = 0; +}; + +} +``` + +### 2.4 布尔运算接口 + +```cpp +namespace vde::boolean { + +enum class BooleanOp { Union, Intersection, Difference, SymDiff }; + +template +class BooleanEngine { +public: + virtual ~BooleanEngine() = default; + virtual GeometryType execute( + const GeometryType& a, const GeometryType& b, BooleanOp op) = 0; + virtual void set_tolerance(double tol) = 0; +}; + +} +``` + +--- + +## 3. 稳定 C API(可选) + +为跨语言绑定提供纯 C 接口: + +```c +// 创建与销毁上下文 +vde_ctx_t vde_engine_create(void); +void vde_engine_destroy(vde_ctx_t ctx); + +// 布尔运算 +vde_mesh_t vde_boolean_union(vde_ctx_t ctx, vde_mesh_t a, vde_mesh_t b); + +// 网格简化 +vde_mesh_t vde_mesh_simplify(vde_ctx_t ctx, vde_mesh_t m, double ratio); +``` + +--- + +## 4. 错误处理约定 + +- 所有公开 API 返回值使用 `Result` 类型(`std::expected` 的兼容实现) +- 错误码枚举定义于 `vde/foundation/error_codes.h` +- 内部实现可使用断言(`assert`),但公开 API 不抛异常 +- `using Result = tl::expected;` + +| 错误码 | 说明 | +|--------|------| +| `kSuccess` | 操作成功 | +| `kInvalidArgument` | 非法参数 | +| `kInvalidGeometry` | 非法几何(退化/非流形) | +| `kDegenerateCase` | 退化情况 | +| `kNotImplemented` | 未实现 | +| `kNumericalError` | 数值计算错误 | diff --git a/docs/03-详细设计/01-数据结构设计.md b/docs/03-详细设计/01-数据结构设计.md new file mode 100644 index 0000000..d0ce72b --- /dev/null +++ b/docs/03-详细设计/01-数据结构设计.md @@ -0,0 +1,180 @@ +# 数据结构设计 + +## 目录 + +1. [基础数学类型](#1-基础数学类型) +2. [半边数据结构](#2-半边数据结构) +3. [NURBS 数据结构](#3-nurbs-数据结构) +4. [BVH 数据结构](#4-bvh-数据结构) +5. [公差系统](#5-公差系统) + +--- + +## 1. 基础数学类型 + +### 1.1 点与向量 + +```cpp +// 基于 Eigen 的类型别名 +template +using Point = Eigen::Matrix; + +template +using Vector = Eigen::Matrix; + +using Point2D = Point; +using Point3D = Point; +using Vector2D = Vector; +using Vector3D = Vector; +using Matrix4D = Eigen::Matrix; +``` + +### 1.2 直线、射线与线段 + +```cpp +// 无限直线 +class Line3D { + Point3D origin_; + Vector3D direction_; // 已归一化 +}; + +// 单向射线(t ≥ 0) +class Ray3D { + Point3D origin_; + Vector3D direction_; // 已归一化 +}; + +// 有限线段 +class Segment3D { + Point3D p0_, p1_; +}; +``` + +### 1.3 包围盒 + +```cpp +class AABB3D { + Point3D min_, max_; + // 核心方法:expand(), center(), extent(), surface_area(), + // volume(), contains(), intersects() +}; +``` + +--- + +## 2. 半边数据结构 + +半边是网格核心数据结构,支持高效的拓扑遍历: + +``` + prev(e) next(e) + ●─────────────●─────────────● + ◄──── e ──── + vert(e) face(e) vert(next(e)) +``` + +### 2.1 数据布局 + +```cpp +class HalfedgeMesh { + std::vector vertices_; // 顶点坐标 + std::vector halfedges_; // 半边数组 + std::vector faces_; // 面数组 + // 惰性计算缓存 + std::vector face_normals_; + std::vector vertex_normals_; + bool normals_dirty_ = true; +}; + +struct Halfedge { + int vertex_index; // 半边指向的顶点 + int face_index; // 半边所属的面(-1 = 边界) + int next_index; // 下一条半边 + int prev_index; // 上一条半边 + int opposite_index; // 对侧半边 +}; + +struct Face { + int halfedge_index; // 该面的起始半边 + int valence; // 边数(三角形=3,四边形=4) +}; +``` + +### 2.2 拓扑遍历模式 + +- **遍历面的边**:`for (he = face.he; ; he = he.next)` +- **遍历顶点邻接面**:`do { he = he.next.opposite; } while (he != start)` +- **遍历顶点一环邻域**:沿半边链遍历相邻顶点 + +--- + +## 3. NURBS 数据结构 + +### 3.1 NURBS 曲线 + +```cpp +class NurbsCurve { + int degree_; // 阶数 + std::vector control_points_; // 控制点 + std::vector knots_; // 节点向量 + std::vector weights_; // 权重 + std::pair domain_; // 有效参数域 [u_p, u_n+1] +}; +``` + +### 3.2 NURBS 曲面 + +```cpp +class NurbsSurface { + int degree_u_, degree_v_; + std::vector> control_points_; // u×v 网格 + std::vector knots_u_, knots_v_; + std::vector> weights_; +}; +``` + +--- + +## 4. BVH 数据结构 + +```cpp +class BVH { + struct Node { + AABB3D bounds; // 包围盒 + int left_child; // 左子节点索引(-1 = 无) + int right_child; // 右子节点索引(-1 = 无) + int first_primitive; // 叶节点:起始图元索引 + int primitive_count; // 叶节点:图元数量 + bool is_leaf() const; + }; + + std::vector nodes_; // 扁平数组(缓存友好) + std::vector primitives_; // 图元数组 + BVHSplitStrategy strategy_; // 分割策略:Middle / Equal / SAH +}; +``` + +**设计要点**:节点存储在连续数组中(而非树形指针),利用 CPU 缓存局部性,大幅加速遍历。 + +--- + +## 5. 公差系统 + +```cpp +class Tolerance { + double absolute_; // 绝对公差(默认 1e-6) + double relative_; // 相对公差(默认 1e-8) + double angular_; // 角度公差(默认 1e-8 rad) + double snapping_; // 吸附距离(默认 1e-4) + +public: + // 两点是否重合 + bool points_equal(const Point3D& a, const Point3D& b) const; + // 值是否为零 + bool is_zero(double value) const; + // 全局单例 + static Tolerance& global(); +}; +``` + +**设计要点**:全局 + 局部两级公差管理。全局公差作为默认值,各个算法可以创建局部实例覆盖。 diff --git a/docs/03-详细设计/02-核心算法设计.md b/docs/03-详细设计/02-核心算法设计.md new file mode 100644 index 0000000..5a0ab20 --- /dev/null +++ b/docs/03-详细设计/02-核心算法设计.md @@ -0,0 +1,141 @@ +# 核心算法设计 + +## 目录 + +1. [Delaunay 三角剖分(2D)](#1-delaunay-三角剖分2d) +2. [NURBS 曲线求值](#2-nurbs-曲线求值) +3. [GJK 碰撞检测](#3-gjk-碰撞检测) +4. [网格简化(QEM)](#4-网格简化qem) +5. [BVH 构建(SAH)](#5-bvh-构建sah) +6. [2D 多边形布尔运算](#6-2d-多边形布尔运算) + +--- + +## 1. Delaunay 三角剖分(2D) + +**算法**:Bowyer-Watson 增量算法 +**复杂度**:O(n log n) 期望,O(n²) 最坏 + +``` +算法步骤: +1. 构造一个覆盖所有输入点的超级三角形 +2. 逐点插入: + a. 找到外接圆包含新点的所有"坏"三角形 + b. 提取坏三角形的边界边(只保留不重复的边) + c. 删除坏三角形 + d. 用新点与每条边界边连接,形成新三角形 +3. 删除包含超级三角形顶点的三角形 +``` + +**关键实现细节**: +- 三角形预存外接圆圆心和半径,加速包含判断 +- 用哈希集合高效提取边界边(出现两次的边=内部边,删除;出现一次的边=边界边,保留) + +--- + +## 2. NURBS 曲线求值 + +**算法**:Cox-de Boor 递推公式 + +``` +求值步骤: +1. find_span(t):二分查找 t 所在的节点区间 +2. basis_functions(t, span):递推计算基函数 N_{i,p} +3. 加权组合:P(t) = Σ N_i·w_i·cp_i / Σ N_i·w_i +``` + +**基函数递推**: + +``` +N_{i,0}(t) = 1 若 knots[i] ≤ t < knots[i+1],否则 0 + +N_{i,p}(t) = (t - knots[i]) / (knots[i+p] - knots[i]) · N_{i,p-1}(t) + + (knots[i+p+1] - t) / (knots[i+p+1] - knots[i+1]) · N_{i+1,p-1}(t) +``` + +**复杂度**:O(p²),p 为阶数 + +--- + +## 3. GJK 碰撞检测 + +**算法**:Gilbert-Johnson-Keerthi +**用途**:检测两个凸体是否相交 + +``` +核心思想: +1. 定义 Minkowski 差:C = A ⊖ B = {a - b | a∈A, b∈B} +2. A 与 B 相交 ⇔ C 包含原点 +3. 迭代构建包含原点的单纯形(点→线→三角形→四面体) +4. 每步使用 support 函数在 Minkowski 差上采样 +5. 3D 中最多约 40 次迭代即可得到结果 +``` + +**Minkowski 差的 support 函数**: +``` +support(d) = support_A(d) - support_B(-d) +``` + +--- + +## 4. 网格简化(QEM) + +**算法**:Quadric Error Metrics(Garland & Heckbert, SIGGRAPH 1997) + +``` +算法步骤: +1. 计算每个顶点的二次误差矩阵 Q(v) +2. 计算每条边的收缩代价:cost(e) = v_opt^T · (Q_v1 + Q_v2) · v_opt +3. 将所有边按代价放入最小堆 +4. 贪心循环: + a. 弹出代价最小的边 + b. 收缩该边到最优位置(或中点) + c. 更新所有受影响边的代价 +5. 重复直到达到目标面数 +``` + +**Q 矩阵定义**:对每个顶点 v,其 Q 矩阵为相邻面平面方程的外积和: +``` +Q(v) = Σ K_p^T · K_p,其中 K_p = [a b c d],ax+by+cz+d=0 +``` + +--- + +## 5. BVH 构建(SAH) + +**算法**:表面积启发式(Surface Area Heuristic) + +``` +SAH 代价函数: +C = C_trav + (n_L·area_L + n_R·area_R) / area_total · C_isect + +构建过程: +1. 对当前节点计算包围盒 +2. 若图元数 ≤ leaf_size 或深度 ≥ max_depth → 创建叶节点 +3. 否则: + a. 沿最长轴排序图元(按质心坐标) + b. 尝试多个分割位置,计算 SAH 代价 + c. 选择代价最低的分割 + d. 如果分割代价 ≥ 不分割的代价 → 创建叶节点 + e. 否则递归构建左右子树 +``` + +--- + +## 6. 2D 多边形布尔运算 + +**算法**:Sutherland-Hodgman 裁剪(当前实现) +**目标算法**:Vatti 扫描线算法(对标 Clipper2) + +``` +当前实现(S-H): +1. 以裁剪多边形为窗口 +2. 对主题多边形的每条边逐一裁剪 +3. 时间复杂度 O(n·m) + +规划实现(Vatti): +1. 扫描线遍历所有边交点 +2. 边分割与标记(内部/外部) +3. 提取结果轮廓 +4. 支持任意多边形(含自交、带孔) +``` diff --git a/docs/03-详细设计/03-类设计.md b/docs/03-详细设计/03-类设计.md new file mode 100644 index 0000000..17ddea1 --- /dev/null +++ b/docs/03-详细设计/03-类设计.md @@ -0,0 +1,138 @@ +# 类设计 + +## 目录 + +1. [类关系总览](#1-类关系总览) +2. [关键设计决策](#2-关键设计决策) +3. [工厂模式](#3-工厂模式) +4. [策略模式](#4-策略模式) +5. [访问者模式](#5-访问者模式) + +--- + +## 1. 类关系总览 + +``` +几何实体层次: + HalfedgeMesh ← 网格实体 + BezierCurve ← Bezier 曲线 + BSplineCurve ← B-Spline 曲线 + NurbsCurve ← NURBS 曲线 + BezierSurface ← Bezier 曲面 + BSplineSurface ← B-Spline 曲面 + Triangle / Polygon ← 基本图形 + +空间索引层次: + SpatialIndex (抽象) + ├── BVH + ├── Octree + ├── KDTree + └── RTree + +布尔运算层次: + BooleanEngine (抽象) + ├── 2D 多边形布尔 + └── 3D 网格布尔 + +工具类: + Transform3D ← 几何变换 + Tolerance ← 公差管理 + GeometryFactory ← 工厂方法 +``` + +--- + +## 2. 关键设计决策 + +### 2.1 值与引用语义 + +| 类型 | 策略 | 理由 | +|------|------|------| +| Point, Vector, AABB | 值语义 | 拷贝开销小(≤ 24 字节) | +| HalfedgeMesh, BVH | 值语义 + 移动 | 大型数据移动比拷贝高效 | +| SpatialIndex | 智能指针 | 多态集合需引用语义 | + +### 2.2 模板化策略 + +- **曲线曲面族**:模板化 + 类型别名 + ```cpp + template class BezierCurve { ... }; + using BezierCurve3D = BezierCurve; + ``` +- **精确谓词**:非模板化,内部使用自适应精度 +- **虚函数**:仅在多态集合场景使用(空间索引接口) +- **性能关键路径**:使用 CRTP 静态多态替代虚函数 + +### 2.3 依赖方向 + +``` +foundation ← core ← curves + ← mesh + ← spatial ← boolean + ← spatial ← collision +``` + +--- + +## 3. 工厂模式 + +```cpp +class GeometryFactory { +public: + // 基本体生成 + static HalfedgeMesh create_box(double w, double h, double d); + static HalfedgeMesh create_sphere(double radius, int segments); + static HalfedgeMesh create_cylinder(double r, double h, int segments); + static HalfedgeMesh create_torus(double R, double r, int segs); + + // 曲线生成 + static NurbsCurve create_circle(double radius); + static NurbsCurve create_line(const Point3D& a, const Point3D& b); + + // 曲面生成 + static NurbsSurface create_ruled_surface( + const NurbsCurve& c1, const NurbsCurve& c2); +}; +``` + +--- + +## 4. 策略模式 + +```cpp +// BVH 构建策略 +enum class BVHSplitStrategy { + Middle, // 中点分割(最快) + Equal, // 等数量分割 + SAH // 表面积启发式(质量最优) +}; + +class BVHBuilder { +public: + BVHBuilder& set_strategy(BVHSplitStrategy s); + BVHBuilder& set_leaf_size(int max_primitives); + BVHBuilder& set_max_depth(int depth); + std::unique_ptr build(const std::vector& primitives); +}; + +// 网格平滑策略 +enum class SmoothMethod { Laplacian, Taubin }; +``` + +--- + +## 5. 访问者模式 + +```cpp +// 网格遍历 +class MeshVisitor { +public: + virtual void visit_vertex(size_t idx, const Point3D& p) = 0; + virtual void visit_face(size_t idx, + const std::vector& vertices) = 0; + virtual void visit_edge(size_t idx, size_t v0, size_t v1) = 0; +}; + +// 使用方式 +mesh.traverse(visitor); +``` diff --git a/docs/04-编码实现/01-编码规范.md b/docs/04-编码实现/01-编码规范.md new file mode 100644 index 0000000..a5c4235 --- /dev/null +++ b/docs/04-编码实现/01-编码规范.md @@ -0,0 +1,115 @@ +# 编码规范 + +## 目录 + +1. [C++ 标准](#1-c-标准) +2. [命名规范](#2-命名规范) +3. [头文件规范](#3-头文件规范) +4. [代码风格](#4-代码风格) +5. [注释规范](#5-注释规范) +6. [内存管理](#6-内存管理) +7. [错误处理](#7-错误处理) +8. [性能规范](#8-性能规范) + +--- + +## 1. C++ 标准 + +- **目标标准**:C++17 +- **编译器**:GCC 9+, Clang 12+, MSVC 2019+ +- **允许的 C++17 特性**: + - structured bindings、`if constexpr`、fold expressions + - `std::string_view`、`std::optional` + - `[[nodiscard]]`、`[[maybe_unused]]` + - CTAD(类模板参数推导) + +--- + +## 2. 命名规范 + +| 元素 | 风格 | 示例 | +|------|------|------| +| 命名空间 | snake_case | `vde::core` | +| 类/结构体 | PascalCase | `HalfedgeMesh` | +| 函数/方法 | snake_case | `compute_convex_hull()` | +| 成员变量 | snake_case_ | `vertices_` | +| 常量 | UPPER_SNAKE_CASE | `MAX_ITERATIONS` | +| 枚举值 | kPascalCase | `kSuccess` | +| 宏 | VDE_UPPER_SNAKE | `VDE_VERSION_MAJOR` | + +--- + +## 3. 头文件规范 + +```cpp +#pragma once // 优先使用 + +// 包含顺序(每组间空行分隔): +// 1. 对应的 .h 文件(.cpp 中) +// 2. C++ 标准库 +// 3. 第三方库(Eigen) +// 4. 本项目头文件 + +#include "vde/core/point.h" + +#include +#include + +#include + +#include "vde/core/aabb.h" +``` + +--- + +## 4. 代码风格 + +- **缩进**:4 空格,不使用 Tab +- **行宽**:上限 100 字符 +- **格式化**:统一使用 `.clang-format`(Google 风格变体) +- **短函数**:单行内联 +- **大括号**:函数/类/结构体后换行,控制流不换行 + +--- + +## 5. 注释规范 + +- **公开 API**:使用 Doxygen 风格(`///`) +- **内部实现**:使用 `//` 行注释 +- **复杂算法**:必须注释算法来源(论文引用或公式) +- **禁止**:注释掉的代码(使用版本控制回溯) + +--- + +## 6. 内存管理 + +- **禁止**裸 `new`/`delete` +- 使用 `std::make_unique` / `std::make_shared` +- 大块连续内存使用 `std::vector` +- 小型对象优先栈分配 +- 性能关键路径使用自定义 `MemoryPool` + +--- + +## 7. 错误处理 + +```cpp +// 公开 API 返回值类型 +template +using Result = tl::expected; + +// 内部断言 +assert(condition && "message"); +``` + +- 公开 API 不抛异常 +- 内部可抛异常,但必须提供异常安全保证 + +--- + +## 8. 性能规范 + +- 函数参数:基本类型传值,大对象传 `const&`,sink 参数传 `&&` +- 循环中避免重复计算:hoist 不变量 +- 容器预分配:`reserve()` 已知大小 +- 避免隐藏的临时对象拷贝 diff --git a/docs/04-编码实现/02-目录结构.md b/docs/04-编码实现/02-目录结构.md new file mode 100644 index 0000000..c8ec5ba --- /dev/null +++ b/docs/04-编码实现/02-目录结构.md @@ -0,0 +1,125 @@ +# 工程目录结构 + +## 目录 + +1. [完整目录树](#1-完整目录树) +2. [CMakeLists.txt 骨架](#2-cmakeliststxt-骨架) + +--- + +## 1. 完整目录树 + +``` +ViewDesignEngine/ +├── CMakeLists.txt # 根 CMake 配置 +├── README.md # 项目说明 +├── .clang-format # 代码格式化配置 +├── .gitignore +│ +├── cmake/ # CMake 模块 +│ └── CompilerSettings.cmake # 编译器警告/优化配置 +│ +├── include/ # 公开头文件 +│ └── vde/ # 命名空间 vde:: +│ ├── foundation/ # 基础设施层 +│ │ ├── math_types.h +│ │ ├── tolerance.h +│ │ ├── error_codes.h +│ │ ├── exact_predicates.h +│ │ ├── memory_pool.h +│ │ ├── io_obj.h +│ │ └── io_stl.h +│ ├── core/ # 几何核心层 +│ │ ├── point.h +│ │ ├── line.h +│ │ ├── plane.h +│ │ ├── triangle.h +│ │ ├── polygon.h +│ │ ├── aabb.h +│ │ ├── transform.h +│ │ ├── distance.h +│ │ └── convex_hull.h +│ ├── curves/ # 曲线曲面层 +│ │ ├── bezier_curve.h +│ │ ├── bspline_curve.h +│ │ ├── nurbs_curve.h +│ │ ├── bezier_surface.h +│ │ ├── bspline_surface.h +│ │ └── tessellation.h +│ ├── mesh/ # 网格处理层 +│ │ ├── halfedge_mesh.h +│ │ ├── delaunay_2d.h +│ │ ├── mesh_simplify.h +│ │ ├── mesh_smooth.h +│ │ ├── mesh_boolean.h +│ │ ├── mesh_quality.h +│ │ └── mesh_repair.h +│ ├── spatial/ # 空间索引层 +│ │ ├── spatial_index.h +│ │ ├── bvh.h +│ │ ├── octree.h +│ │ ├── kd_tree.h +│ │ └── r_tree.h +│ ├── boolean/ # 布尔运算层 +│ │ ├── boolean_2d.h +│ │ └── boolean_mesh.h +│ └── collision/ # 碰撞检测层 +│ ├── gjk.h +│ ├── sat.h +│ ├── ray_intersect.h +│ └── tri_intersect.h +│ +├── src/ # 实现文件(与 include 镜像) +│ +├── tests/ # 单元测试 +│ ├── core/ # 测试几何核心 +│ ├── curves/ # 测试曲线曲面 +│ ├── mesh/ # 测试网格处理 +│ ├── spatial/ # 测试空间索引 +│ ├── collision/ # 测试碰撞检测 +│ └── boolean/ # 测试布尔运算 +│ +├── examples/ # 示例程序 +│ ├── 01_hello_triangle/ +│ ├── 02_bezier/ +│ ├── 03_mesh/ +│ ├── 04_delaunay/ +│ ├── 05_boolean/ +│ └── 06_collision/ +│ +├── benchmarks/ # 性能测试 +├── data/ # 测试数据 +│ ├── meshes/ +│ └── curves/ +└── docs/ # 工程文档 +``` + +--- + +## 2. CMakeLists.txt 骨架 + +```cmake +cmake_minimum_required(VERSION 3.16) +project(ViewDesignEngine VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 选项 +option(BUILD_TESTS "构建单元测试" ON) +option(BUILD_EXAMPLES "构建示例程序" ON) + +# 依赖(自动 FetchContent) +find_package(Eigen3 3.3 QUIET) +# 若未找到,使用 FetchContent 下载 + +# 子目录 +add_subdirectory(src) +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() +if(BUILD_EXAMPLES) + add_subdirectory(examples) +endif() +``` diff --git a/docs/05-测试验证/01-测试计划.md b/docs/05-测试验证/01-测试计划.md new file mode 100644 index 0000000..977366e --- /dev/null +++ b/docs/05-测试验证/01-测试计划.md @@ -0,0 +1,128 @@ +# 测试计划 + +## 目录 + +1. [测试策略](#1-测试策略) +2. [测试环境](#2-测试环境) +3. [单元测试规范](#3-单元测试规范) +4. [性能基准](#4-性能基准) +5. [回归测试](#5-回归测试) +6. [测试数据管理](#6-测试数据管理) + +--- + +## 1. 测试策略 + +### 测试金字塔 + +``` + ┌───────┐ + │ 端到端 │ 完整建模工作流测试 + ├───────┤ + │ 集成 │ 模块间交互测试 + ├───────┤ + │ 单元 │ 每个函数/类的独立测试(主力) + └───────┘ +``` + +| 层次 | 范围 | 工具 | 覆盖目标 | +|------|------|------|---------| +| 单元测试 | 单个函数/类 | Google Test | ≥ 90% | +| 集成测试 | 模块间交互 | Google Test | 关键路径 | +| 端到端测试 | 完整工作流 | 自定义脚本 | 典型场景 | +| 性能测试 | 单个算法 | Google Benchmark | 回归监控 | + +--- + +## 2. 测试环境 + +| 环境 | 编译器 | 平台 | +|------|--------|------| +| CI linux-gcc | GCC 11 | Ubuntu 22.04 | +| CI linux-clang | Clang 15 | Ubuntu 22.04 | +| CI windows-msvc | MSVC 2022 | Windows Server 2022 | +| CI macos-clang | Apple Clang 14 | macOS 13 | + +### CI 流程 + +``` +Push / PR + ├──► 编译(Debug + Release) + ├──► 单元测试 + ├──► 代码覆盖率(lcov → Codecov) + ├──► 静态分析(clang-tidy) + └──► 性能基准(Release 构建,与基线对比) +``` + +--- + +## 3. 单元测试规范 + +### 命名规则 + +``` +TEST(<模块名>Test, <函数名>_<场景描述>) +``` + +示例:`TEST(MeshTest, Simplify_ReducesFaceCount)` + +### AAA 结构 + +```cpp +TEST(PointTest, Distance_SamePoint_ReturnsZero) { + // Arrange(准备) + Point3D a(1, 2, 3), b(1, 2, 3); + // Act(执行) + double d = (a - b).norm(); + // Assert(断言) + EXPECT_DOUBLE_EQ(d, 0.0); +} +``` + +### 必测场景 + +1. **正常输入**(Happy Path) +2. **边界条件**(空集、单元素、极值) +3. **退化情况**(共线、共面、零面积) +4. **错误输入**(非法参数、无效状态) +5. **精度边界**(极小/极大坐标值) + +--- + +## 4. 性能基准 + +| 算法 | 输入规模 | 性能目标 | +|------|---------|---------| +| Delaunay 2D | 10⁶ 点 | ≤ 10 秒 | +| BVH 构建 | 10⁶ 三角面 | ≤ 5 秒 | +| BVH 射线查询 | 10⁶ 三角面 | ≤ 0.1 毫秒/次 | +| GJK 碰撞检测 | 凸体 | ≤ 30 次迭代 | +| 布尔运算 2D | 10³ 顶点 | ≤ 0.1 秒 | +| NURBS 求值 | — | ≤ 1 微秒/次 | + +--- + +## 5. 回归测试 + +- **Golden File 测试**:将标准测试用例输出与参考文件比对 +- **性能回归**:基准测试结果与历史对比,性能退化 > 10% 报警 + +--- + +## 6. 测试数据管理 + +``` +data/ +├── meshes/ +│ ├── primitives/ # 基本体(球、盒、柱) +│ ├── stanford/ # Stanford 标准模型 +│ │ ├── bunny.obj +│ │ ├── dragon.obj +│ │ └── armadillo.obj +│ └── degenerate/ # 退化情况 +│ ├── non_manifold.obj +│ └── zero_area.obj +└── curves/ + ├── bezier_basic.json + └── nurbs_circle.json +``` diff --git a/docs/05-测试验证/02-测试用例.md b/docs/05-测试验证/02-测试用例.md new file mode 100644 index 0000000..d9e5fa1 --- /dev/null +++ b/docs/05-测试验证/02-测试用例.md @@ -0,0 +1,138 @@ +# 测试用例 + +## 目录 + +1. [TC-01:Point 基础运算](#tc-01point-基础运算) +2. [TC-02:Delaunay 三角剖分](#tc-02delaunay-三角剖分) +3. [TC-03:NURBS 曲线求值](#tc-03nurbs-曲线求值) +4. [TC-04:半边网格拓扑](#tc-04半边网格拓扑) +5. [TC-05:GJK 碰撞检测](#tc-05gjk-碰撞检测) +6. [TC-06:网格简化](#tc-06网格简化) +7. [TC-07:2D 多边形布尔运算](#tc-072d-多边形布尔运算) +8. [TC-08:BVH 空间查询](#tc-08bvh-空间查询) + +--- + +## TC-01:Point 基础运算 + +| 项目 | 内容 | +|------|------| +| **模块** | core | +| **被测函数** | Point3D 算术与距离 | +| **优先级** | P0 | + +| 编号 | 输入 | 预期输出 | +|------|------|---------| +| TC-01.1 | a(0,0,0), b(0,0,0) | 距离 = 0 | +| TC-01.2 | a(0,0,0), b(3,4,0) | 距离 = 5.0 | +| TC-01.3 | a(1,1,1) + Vector(4,5,6) | Point(5,6,7) | +| TC-01.4 | equals(a, a+1e-7, tol=1e-6) | true | + +--- + +## TC-02:Delaunay 三角剖分 + +| 项目 | 内容 | +|------|------| +| **模块** | mesh | +| **优先级** | P0 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-02.1 | 3 个非共线点 | 1 个三角形 | +| TC-02.2 | 4 个点(正方形顶点) | 2 个三角形,满足空外接圆 | +| TC-02.3 | 100 个随机点 | 所有三角形满足 Delaunay 条件 | +| TC-02.4 | 0 个点 | 返回空集合 | +| TC-02.5 | 大规模:10⁵ 个点 | 完成时间 ≤ 5 秒 | + +--- + +## TC-03:NURBS 曲线求值 + +| 项目 | 内容 | +|------|------| +| **模块** | curves | +| **优先级** | P0 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-03.1 | 3 阶 NURBS, t = domain.min | 接近首个控制点 | +| TC-03.2 | 3 阶 NURBS, t = domain.max | 接近末个控制点 | +| TC-03.3 | 权重全为 1 的 NURBS | 与 B-Spline 求值结果一致 | +| TC-03.4 | t 超出 domain | 返回域边界值 | + +--- + +## TC-04:半边网格拓扑 + +| 项目 | 内容 | +|------|------| +| **模块** | mesh | +| **优先级** | P0 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-04.1 | 单个三角形 | 3 顶点, 1 面, 全部为边界 | +| TC-04.2 | 两个相邻三角形 | 4 顶点, 2 面, 1 条内部边 | +| TC-04.3 | 遍历面的边 | 半边顺序正确 | +| TC-04.4 | 法向量计算 | 面法向为单位向量 | + +--- + +## TC-05:GJK 碰撞检测 + +| 项目 | 内容 | +|------|------| +| **模块** | collision | +| **优先级** | P0 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-05.1 | 两个分离球体 | 返回 false | +| TC-05.2 | 两个相交球体 | 返回 true | +| TC-05.3 | 小球在大球内部 | 返回 true | + +--- + +## TC-06:网格简化 + +| 项目 | 内容 | +|------|------| +| **模块** | mesh | +| **优先级** | P1 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-06.1 | 简化率 0.5 | 面数 ≈ 原面数 × 0.5 | +| TC-06.2 | 简化率 0 | 返回原始网格 | +| TC-06.3 | 简化后检查 | 无退化面(面积 > 0) | + +--- + +## TC-07:2D 多边形布尔运算 + +| 项目 | 内容 | +|------|------| +| **模块** | boolean | +| **优先级** | P1 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-07.1 | 两个相交矩形,求交集 | 交集区域,面积正确 | +| TC-07.2 | 两个分离矩形,求交集 | 结果为空 | +| TC-07.3 | 大矩形包含小矩形,求差集 | 带孔矩形 | + +--- + +## TC-08:BVH 空间查询 + +| 项目 | 内容 | +|------|------| +| **模块** | spatial | +| **优先级** | P1 | + +| 编号 | 输入 | 预期 | +|------|------|------| +| TC-08.1 | 射线穿过三角形 | 返回该三角形 | +| TC-08.2 | 射线不穿过任何三角形 | 返回空 | +| TC-08.3 | 空 BVH 查询 | 返回空 | diff --git a/docs/06-部署维护/01-构建指南.md b/docs/06-部署维护/01-构建指南.md new file mode 100644 index 0000000..5380a56 --- /dev/null +++ b/docs/06-部署维护/01-构建指南.md @@ -0,0 +1,110 @@ +# 构建指南 + +## 目录 + +1. [环境要求](#1-环境要求) +2. [获取源码](#2-获取源码) +3. [构建步骤](#3-构建步骤) +4. [CMake 选项](#4-cmake-选项) +5. [作为依赖集成](#5-作为依赖集成) +6. [文档生成](#6-文档生成) +7. [故障排查](#7-故障排查) + +--- + +## 1. 环境要求 + +| 依赖 | 最低版本 | 说明 | +|------|---------|------| +| CMake | 3.16+ | 构建系统 | +| C++ 编译器 | GCC 9+ / Clang 12+ / MSVC 2019+ | C++17 支持 | +| Eigen | 3.3+(自动下载) | 线性代数(header-only) | +| Google Test | 自动下载 | 单元测试 | +| Doxygen | 1.9+ | API 文档生成(可选) | + +--- + +## 2. 获取源码 + +```bash +git clone <仓库地址> +cd ViewDesignEngine +``` + +--- + +## 3. 构建步骤 + +### Linux / macOS + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTS=ON \ + -DBUILD_EXAMPLES=ON +cmake --build build -j$(nproc) +cd build && ctest --output-on-failure +``` + +### Windows + +```powershell +cmake -B build -G "Visual Studio 17 2022" +cmake --build build --config Release +cd build && ctest --config Release +``` + +--- + +## 4. CMake 选项 + +| 选项 | 默认值 | 说明 | +|------|--------|------| +| `BUILD_TESTS` | ON | 构建单元测试 | +| `BUILD_EXAMPLES` | ON | 构建示例程序 | +| `BUILD_BENCHMARKS` | OFF | 构建性能测试 | +| `BUILD_SHARED_LIBS` | OFF | 构建共享库(默认静态库) | +| `ENABLE_SANITIZERS` | OFF | 启用 ASan/UBSan | +| `ENABLE_LTO` | ON | 启用链接时优化 | + +--- + +## 5. 作为依赖集成 + +### CMake FetchContent + +```cmake +include(FetchContent) +FetchContent_Declare(ViewDesignEngine + GIT_REPOSITORY <仓库地址> + GIT_TAG v0.1.0 +) +FetchContent_MakeAvailable(ViewDesignEngine) +target_link_libraries(你的项目 PRIVATE vde) +``` + +### find_package(安装后) + +```cmake +find_package(ViewDesignEngine REQUIRED) +target_link_libraries(你的项目 PRIVATE vde::engine) +``` + +--- + +## 6. 文档生成 + +```bash +cmake -B build -DVDE_BUILD_DOCS=ON +cmake --build build --target doc +# 输出:build/docs/html/index.html +``` + +--- + +## 7. 故障排查 + +| 问题 | 解决方案 | +|------|---------| +| Eigen 未找到 | 将自动通过 FetchContent 下载 | +| 编译时间过长 | 使用 `-j` 并行编译,关闭 LTO | +| 链接错误 | 确认 C++17 标准库链接正确 | diff --git a/docs/06-部署维护/02-API参考.md b/docs/06-部署维护/02-API参考.md new file mode 100644 index 0000000..a94cb95 --- /dev/null +++ b/docs/06-部署维护/02-API参考.md @@ -0,0 +1,194 @@ +# API 参考 + +## 目录 + +1. [命名空间](#1-命名空间) +2. [core 模块](#2-core-模块) +3. [curves 模块](#3-curves-模块) +4. [mesh 模块](#4-mesh-模块) +5. [spatial 模块](#5-spatial-模块) +6. [boolean 模块](#6-boolean-模块) +7. [collision 模块](#7-collision-模块) +8. [foundation 模块](#8-foundation-模块) +9. [错误码](#9-错误码) + +--- + +## 1. 命名空间 + +```cpp +namespace vde::foundation { ... } // 基础设施 +namespace vde::core { ... } // 几何核心 +namespace vde::curves { ... } // 曲线曲面 +namespace vde::mesh { ... } // 网格处理 +namespace vde::spatial { ... } // 空间索引 +namespace vde::boolean { ... } // 布尔运算 +namespace vde::collision { ... } // 碰撞检测 +``` + +--- + +## 2. core 模块 + +### 基本类型 + +| 类型 | 说明 | +|------|------| +| `Point2D`, `Point3D` | 二维/三维点 | +| `Vector2D`, `Vector3D` | 二维/三维向量 | +| `Line3Dd` | 三维无限直线 | +| `Ray3Dd` | 三维射线 | +| `Segment3Dd` | 三维线段 | +| `Plane3D` | 三维平面 | +| `Triangle3D` | 三维三角形 | +| `AABB3D` | 轴对齐包围盒 | + +### 变换 + +```cpp +Transform3D translate(double x, double y, double z); +Transform3D rotate(const Vector3D& axis, double rad); +Transform3D scale(double sx, double sy, double sz); + +// 组合 +Matrix4D m = rotate_x(90_deg) * translate(1, 0, 0); +Point3D p2 = m * p1; +``` + +### 凸包与距离 + +```cpp +// 2D 凸包(Graham Scan) +std::vector convex_hull_2d(const std::vector& pts); + +// 距离计算 +double distance(const Point3D& a, const Point3D& b); +double distance(const Point3D& p, const Plane3D& plane); +double distance(const Point3D& p, const Triangle3D& tri); +Point3D closest_point(const Point3D& p, const Triangle3D& tri); +``` + +--- + +## 3. curves 模块 + +```cpp +// 曲线构造 +BezierCurve bezier(control_points); +NurbsCurve nurbs(control_points, knots, weights, degree); + +// 求值与求导 +Point3D p = curve.evaluate(t); +Vector3D d1 = curve.derivative(t, 1); + +// 曲面 +NurbsSurface surface(grid, knots_u, knots_v, weights, deg_u, deg_v); +Point3D p = surface.evaluate(u, v); + +// 离散化 +auto [verts, tris] = tessellate(eval_func, res_u, res_v); +``` + +--- + +## 4. mesh 模块 + +```cpp +HalfedgeMesh mesh; + +// 查询 +size_t nv = mesh.num_vertices(); +const Point3D& v = mesh.vertex(idx); + +// Delaunay +auto [verts, tris] = delaunay_2d(points); + +// 简化与平滑 +auto simplified = simplify_mesh(mesh, {.target_ratio = 0.5}); +auto smoothed = smooth_mesh(mesh, {.iterations = 10}); + +// 质量评估 +auto quality = evaluate_mesh_quality(mesh); +``` + +--- + +## 5. spatial 模块 + +```cpp +BVH bvh; +bvh.build(triangles); + +auto hits = bvh.query_ray(ray); // 所有命中 +auto nearest = bvh.query_ray_nearest(ray); // 最近命中 +auto in_box = bvh.query_range(aabb); // 范围查询 +auto knn = bvh.query_knn(point, 5); // K 近邻 +``` + +--- + +## 6. boolean 模块 + +```cpp +// 2D 多边形布尔 +auto result = boolean_2d(poly_a, poly_b, BooleanOp::Union); +auto result = boolean_2d(poly_a, poly_b, BooleanOp::Intersection); +auto result = boolean_2d(poly_a, poly_b, BooleanOp::Difference); +``` + +--- + +## 7. collision 模块 + +```cpp +// GJK 碰撞检测 +bool hit = gjk_intersect(convex_a, convex_b); + +// 射线-三角形相交(Möller-Trumbore) +auto hit = ray_triangle_intersect(ray, tri); +// hit->t, hit->point, hit->u, hit->v + +// 三角形-三角形相交 +bool hit = tri_tri_intersect(t1, t2); +``` + +--- + +## 8. foundation 模块 + +### 公差系统 + +```cpp +Tolerance tol(/* absolute */ 1e-6, /* relative */ 1e-8); +bool eq = tol.points_equal(a, b); +bool zero = tol.is_zero(value); +``` + +### 精确谓词 + +```cpp +auto orient = exact::orient_2d(a, b, c); +auto in = exact::in_circle(a, b, c, d); +``` + +### I/O + +```cpp +auto mesh = read_obj("model.obj"); +write_obj(mesh, "output.obj"); +auto tris = read_stl("model.stl"); +``` + +--- + +## 9. 错误码 + +| 错误码 | 说明 | +|--------|------| +| `kSuccess` | 操作成功 | +| `kInvalidArgument` | 非法参数 | +| `kInvalidGeometry` | 非法几何(退化/非流形) | +| `kDegenerateCase` | 退化情况 | +| `kNotImplemented` | 未实现 | +| `kNumericalError` | 数值计算错误 | +| `kOutOfMemory` | 内存不足 | diff --git a/examples/01_hello_triangle/CMakeLists.txt b/examples/01_hello_triangle/CMakeLists.txt new file mode 100644 index 0000000..d159efa --- /dev/null +++ b/examples/01_hello_triangle/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(hello_triangle main.cpp) +target_link_libraries(hello_triangle PRIVATE vde) diff --git a/examples/01_hello_triangle/main.cpp b/examples/01_hello_triangle/main.cpp new file mode 100644 index 0000000..a747286 --- /dev/null +++ b/examples/01_hello_triangle/main.cpp @@ -0,0 +1,15 @@ +#include +#include "vde/core/point.h" +#include "vde/core/triangle.h" +#include "vde/core/aabb.h" + +int main() { + using namespace vde::core; + Triangle3D tri({0,0,0}, {1,0,0}, {0,1,0}); + std::cout << "Triangle area: " << tri.area() << "\n"; + std::cout << "Centroid: (" << tri.centroid().transpose() << ")\n"; + Point3D p(0.25, 0.25, 0); + std::cout << "Point (0.25, 0.25) in triangle: " + << (tri.contains(p) ? "yes" : "no") << "\n"; + return 0; +} diff --git a/examples/02_bezier/CMakeLists.txt b/examples/02_bezier/CMakeLists.txt new file mode 100644 index 0000000..1c3c3da --- /dev/null +++ b/examples/02_bezier/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(bezier main.cpp) +target_link_libraries(bezier PRIVATE vde) diff --git a/examples/02_bezier/main.cpp b/examples/02_bezier/main.cpp new file mode 100644 index 0000000..b5cb868 --- /dev/null +++ b/examples/02_bezier/main.cpp @@ -0,0 +1,16 @@ +#include +#include "vde/curves/bezier_curve.h" + +int main() { + using namespace vde::curves; + BezierCurve curve({ + {0,0,0}, {1,2,0}, {3,2,0}, {4,0,0} + }); + std::cout << "Cubic Bezier, degree=" << curve.degree() << "\n"; + for (int i = 0; i <= 10; ++i) { + double t = i / 10.0; + auto p = curve.evaluate(t); + std::cout << "t=" << t << " -> (" << p.transpose() << ")\n"; + } + return 0; +} diff --git a/examples/03_mesh/CMakeLists.txt b/examples/03_mesh/CMakeLists.txt new file mode 100644 index 0000000..4edff35 --- /dev/null +++ b/examples/03_mesh/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(mesh_basics main.cpp) +target_link_libraries(mesh_basics PRIVATE vde) diff --git a/examples/03_mesh/main.cpp b/examples/03_mesh/main.cpp new file mode 100644 index 0000000..4bf3f17 --- /dev/null +++ b/examples/03_mesh/main.cpp @@ -0,0 +1,18 @@ +#include +#include "vde/mesh/halfedge_mesh.h" + +int main() { + using namespace vde::mesh; + HalfedgeMesh mesh; + mesh.build_from_triangles( + {{0,0,0},{1,0,0},{0,1,0},{1,1,0}}, + {{0,1,2},{1,3,2}} + ); + std::cout << "V: " << mesh.num_vertices() + << " F: " << mesh.num_faces() + << " E: " << mesh.num_edges() << "\n"; + auto b = mesh.bounds(); + std::cout << "Bounds: (" << b.min().transpose() << ") - (" + << b.max().transpose() << ")\n"; + return 0; +} diff --git a/examples/04_delaunay/CMakeLists.txt b/examples/04_delaunay/CMakeLists.txt new file mode 100644 index 0000000..3514822 --- /dev/null +++ b/examples/04_delaunay/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(delaunay main.cpp) +target_link_libraries(delaunay PRIVATE vde) diff --git a/examples/04_delaunay/main.cpp b/examples/04_delaunay/main.cpp new file mode 100644 index 0000000..91c6a9b --- /dev/null +++ b/examples/04_delaunay/main.cpp @@ -0,0 +1,17 @@ +#include +#include +#include "vde/mesh/delaunay_2d.h" + +int main() { + using namespace vde::mesh; + std::vector pts; + std::mt19937 rng(42); + std::uniform_real_distribution dist(0, 1); + for (int i = 0; i < 100; ++i) + pts.emplace_back(dist(rng), dist(rng)); + + auto result = delaunay_2d(pts); + std::cout << "Delaunay: " << result.vertices.size() + << " vertices, " << result.triangles.size() << " triangles\n"; + return 0; +} diff --git a/examples/05_boolean/CMakeLists.txt b/examples/05_boolean/CMakeLists.txt new file mode 100644 index 0000000..68e1c71 --- /dev/null +++ b/examples/05_boolean/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(boolean_ops main.cpp) +target_link_libraries(boolean_ops PRIVATE vde) diff --git a/examples/05_boolean/main.cpp b/examples/05_boolean/main.cpp new file mode 100644 index 0000000..92fcaae --- /dev/null +++ b/examples/05_boolean/main.cpp @@ -0,0 +1,13 @@ +#include +#include "vde/boolean/boolean_2d.h" + +int main() { + using namespace vde::boolean; + Polygon2D a({{0,0},{3,0},{3,3},{0,3}}); + Polygon2D b({{1,1},{4,1},{4,4},{1,4}}); + auto result = boolean_2d(a, b, BooleanOp::Intersection); + std::cout << "Intersection result polygons: " << result.size() << "\n"; + if (!result.empty()) + std::cout << "Result vertices: " << result[0].size() << "\n"; + return 0; +} diff --git a/examples/06_collision/CMakeLists.txt b/examples/06_collision/CMakeLists.txt new file mode 100644 index 0000000..daf5c1d --- /dev/null +++ b/examples/06_collision/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(collision main.cpp) +target_link_libraries(collision PRIVATE vde) diff --git a/examples/06_collision/main.cpp b/examples/06_collision/main.cpp new file mode 100644 index 0000000..ec43d8a --- /dev/null +++ b/examples/06_collision/main.cpp @@ -0,0 +1,27 @@ +#include +#include "vde/collision/gjk.h" +#include "vde/collision/ray_intersect.h" + +int main() { + using namespace vde::collision; + // Sphere support + auto sphere = [](const Point3D& c, double r) -> SupportFunc { + return [=](const Vector3D& d) { return c + d.normalized() * r; }; + }; + + auto a = sphere({0,0,0}, 1.0); + auto b = sphere({2,0,0}, 1.0); + std::cout << "Spheres intersect: " << (gjk_intersect(a, b) ? "yes" : "no") << "\n"; + + auto c = sphere({0,0,0}, 1.0); + auto d = sphere({0.5,0,0}, 1.0); + std::cout << "Overlapping spheres intersect: " << (gjk_intersect(c, d) ? "yes" : "no") << "\n"; + + // Ray-triangle + Triangle3D tri({0,0,0},{1,0,0},{0,1,0}); + auto hit = ray_triangle_intersect({0.25,0.25,-1}, Vector3D(0,0,1), tri); + std::cout << "Ray hits triangle: " << (hit.has_value() ? "yes" : "no") << "\n"; + if (hit) std::cout << " at (" << hit->point.transpose() << "), t=" << hit->t << "\n"; + + return 0; +} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..5ebde21 --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,6 @@ +add_subdirectory(01_hello_triangle) +add_subdirectory(02_bezier) +add_subdirectory(03_mesh) +add_subdirectory(04_delaunay) +add_subdirectory(05_boolean) +add_subdirectory(06_collision) diff --git a/include/vde/boolean/boolean_2d.h b/include/vde/boolean/boolean_2d.h new file mode 100644 index 0000000..1ac1f21 --- /dev/null +++ b/include/vde/boolean/boolean_2d.h @@ -0,0 +1,13 @@ +#pragma once +#include "vde/core/polygon.h" +#include + +namespace vde::boolean { + +enum class BooleanOp { Union, Intersection, Difference, SymDiff }; + +/// 2D polygon boolean operations (Vatti algorithm) +/// Currently implemented via a simple edge walking approach for convex polygons +std::vector boolean_2d(const Polygon2D& a, const Polygon2D& b, BooleanOp op); + +} // namespace vde::boolean diff --git a/include/vde/boolean/boolean_mesh.h b/include/vde/boolean/boolean_mesh.h new file mode 100644 index 0000000..a1ac052 --- /dev/null +++ b/include/vde/boolean/boolean_mesh.h @@ -0,0 +1,10 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" + +namespace vde::boolean { + +enum class BooleanOp { Union, Intersection, Difference, SymDiff }; + +HalfedgeMesh mesh_boolean(const HalfedgeMesh& a, const HalfedgeMesh& b, BooleanOp op); + +} // namespace vde::boolean diff --git a/include/vde/collision/gjk.h b/include/vde/collision/gjk.h new file mode 100644 index 0000000..bb1ed4b --- /dev/null +++ b/include/vde/collision/gjk.h @@ -0,0 +1,27 @@ +#pragma once +#include "vde/core/point.h" +#include +#include + +namespace vde::collision { + +/// Support function type: given direction, return farthest point on shape +using SupportFunc = std::function; + +struct GJKResult { + bool intersect; + double distance; + Point3D point_a; + Point3D point_b; +}; + +/// GJK intersection test for convex shapes +bool gjk_intersect(const SupportFunc& shape_a, const SupportFunc& shape_b); + +/// GJK distance between convex shapes +double gjk_distance(const SupportFunc& shape_a, const SupportFunc& shape_b); + +/// Full GJK: intersection + closest points + penetration (with EPA) +GJKResult gjk_full(const SupportFunc& shape_a, const SupportFunc& shape_b); + +} // namespace vde::collision diff --git a/include/vde/collision/ray_intersect.h b/include/vde/collision/ray_intersect.h new file mode 100644 index 0000000..36450b4 --- /dev/null +++ b/include/vde/collision/ray_intersect.h @@ -0,0 +1,24 @@ +#pragma once +#include "vde/core/point.h" +#include "vde/core/line.h" +#include "vde/core/triangle.h" +#include + +namespace vde::collision { + +struct RayTriResult { + double t; // parameter along ray + double u, v; // barycentric + Point3D point; +}; + +/// Möller–Trumbore ray-triangle intersection +std::optional ray_triangle_intersect( + const Point3D& origin, const Vector3D& dir, const Triangle3D& tri); + +inline std::optional ray_triangle_intersect( + const Ray3Dd& ray, const Triangle3D& tri) { + return ray_triangle_intersect(ray.origin(), ray.direction(), tri); +} + +} // namespace vde::collision diff --git a/include/vde/collision/sat.h b/include/vde/collision/sat.h new file mode 100644 index 0000000..32799e3 --- /dev/null +++ b/include/vde/collision/sat.h @@ -0,0 +1,14 @@ +#pragma once +#include "vde/core/point.h" +#include +#include + +namespace vde::collision { + +/// SAT (Separating Axis Theorem) for convex polyhedra +bool sat_intersect(const std::vector& verts_a, + const std::vector>& faces_a, + const std::vector& verts_b, + const std::vector>& faces_b); + +} // namespace vde::collision diff --git a/include/vde/collision/tri_intersect.h b/include/vde/collision/tri_intersect.h new file mode 100644 index 0000000..a8adb30 --- /dev/null +++ b/include/vde/collision/tri_intersect.h @@ -0,0 +1,9 @@ +#pragma once +#include "vde/core/triangle.h" + +namespace vde::collision { + +/// Triangle-triangle intersection test (Möller) +bool tri_tri_intersect(const Triangle3D& t1, const Triangle3D& t2); + +} // namespace vde::collision diff --git a/include/vde/core/aabb.h b/include/vde/core/aabb.h new file mode 100644 index 0000000..f838f21 --- /dev/null +++ b/include/vde/core/aabb.h @@ -0,0 +1,54 @@ +#pragma once +#include "vde/core/point.h" +#include +#include + +namespace vde::core { + +template +class AABB { +public: + AABB() : min_(Point3D::Constant(std::numeric_limits::max())), + max_(Point3D::Constant(std::numeric_limits::lowest())) {} + + AABB(const Point3D& min, const Point3D& max) : min_(min), max_(max) {} + + void expand(const Point3D& p) { + min_ = min_.cwiseMin(p); + max_ = max_.cwiseMax(p); + } + + void expand(const AABB& other) { + min_ = min_.cwiseMin(other.min_); + max_ = max_.cwiseMax(other.max_); + } + + [[nodiscard]] Point3D center() const { return (min_ + max_) * 0.5; } + [[nodiscard]] Vector3D extent() const { return max_ - min_; } + [[nodiscard]] T surface_area() const { + Vector3D e = extent(); + return 2.0 * (e.x() * e.y() + e.y() * e.z() + e.z() * e.x()); + } + [[nodiscard]] T volume() const { + Vector3D e = extent(); + return e.x() * e.y() * e.z(); + } + [[nodiscard]] bool contains(const Point3D& p) const { + return (p.array() >= min_.array()).all() && (p.array() <= max_.array()).all(); + } + [[nodiscard]] bool intersects(const AABB& other) const { + return (min_.array() <= other.max_.array()).all() + && (max_.array() >= other.min_.array()).all(); + } + + [[nodiscard]] const Point3D& min() const { return min_; } + [[nodiscard]] const Point3D& max() const { return max_; } + +private: + Point3D min_, max_; +}; + +using AABB3D = AABB; +using AABB3f = AABB; + +} // namespace vde::core diff --git a/include/vde/core/convex_hull.h b/include/vde/core/convex_hull.h new file mode 100644 index 0000000..fd32e5d --- /dev/null +++ b/include/vde/core/convex_hull.h @@ -0,0 +1,14 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::core { + +/// 2D convex hull — Graham scan, O(n log n) +std::vector convex_hull_2d(const std::vector& points); + +/// 3D convex hull — QuickHull +/// Returns triangles of the hull (counter-clockwise when viewed from outside) +std::vector> convex_hull_3d(const std::vector& points); + +} // namespace vde::core diff --git a/include/vde/core/distance.h b/include/vde/core/distance.h new file mode 100644 index 0000000..232f931 --- /dev/null +++ b/include/vde/core/distance.h @@ -0,0 +1,18 @@ +#pragma once +#include "vde/core/point.h" +#include "vde/core/line.h" +#include "vde/core/plane.h" +#include "vde/core/triangle.h" + +namespace vde::core { + +double distance(const Point3D& a, const Point3D& b); +double distance(const Point3D& p, const Line3D& line); +double distance(const Point3D& p, const Segment3D& seg); +double distance(const Point3D& p, const Plane& plane); +double distance(const Point3D& p, const Triangle& tri); + +Point3D closest_point(const Point3D& p, const Triangle& tri); +Point3D closest_point(const Point3D& p, const Segment3D& seg); + +} // namespace vde::core diff --git a/include/vde/core/line.h b/include/vde/core/line.h new file mode 100644 index 0000000..06bd563 --- /dev/null +++ b/include/vde/core/line.h @@ -0,0 +1,40 @@ +#pragma once +#include "vde/core/point.h" + +namespace vde::core { + +template +class Line3D { +public: + Line3D() = default; + Line3D(const Point3D& origin, const Vector3D& direction) + : origin_(origin), direction_(direction.normalized()) {} + + [[nodiscard]] const Point3D& origin() const { return origin_; } + [[nodiscard]] const Vector3D& direction() const { return direction_; } + [[nodiscard]] Point3D point_at(T t) const { return origin_ + direction_ * t; } + +private: + Point3D origin_{0,0,0}; + Vector3D direction_{1,0,0}; +}; + +template +class Segment3D { +public: + Segment3D() = default; + Segment3D(const Point3D& p0, const Point3D& p1) : p0_(p0), p1_(p1) {} + + [[nodiscard]] const Point3D& p0() const { return p0_; } + [[nodiscard]] const Point3D& p1() const { return p1_; } + [[nodiscard]] Vector3D direction() const { return p1_ - p0_; } + [[nodiscard]] T length() const { return direction().norm(); } + +private: + Point3D p0_{0,0,0}, p1_{0,0,0}; +}; + +using Line3Dd = Line3D; +using Segment3Dd = Segment3D; + +} // namespace vde::core diff --git a/include/vde/core/plane.h b/include/vde/core/plane.h new file mode 100644 index 0000000..d433f81 --- /dev/null +++ b/include/vde/core/plane.h @@ -0,0 +1,30 @@ +#pragma once +#include "vde/core/point.h" + +namespace vde::core { + +template +class Plane { +public: + Plane() = default; + Plane(const Point3D& point, const Vector3D& normal) + : normal_(normal.normalized()), d_(-normal_.dot(point)) {} + Plane(const Point3D& a, const Point3D& b, const Point3D& c) + : Plane(a, (b - a).cross(c - a)) {} + + [[nodiscard]] const Vector3D& normal() const { return normal_; } + [[nodiscard]] T d() const { return d_; } + [[nodiscard]] T signed_distance(const Point3D& p) const { return normal_.dot(p) + d_; } + [[nodiscard]] T distance(const Point3D& p) const { return std::abs(signed_distance(p)); } + [[nodiscard]] Point3D project(const Point3D& p) const { + return p - normal_ * signed_distance(p); + } + +private: + Vector3D normal_{0,0,1}; + T d_{0}; +}; + +using Plane3D = Plane; + +} // namespace vde::core diff --git a/include/vde/core/point.h b/include/vde/core/point.h new file mode 100644 index 0000000..7f2680d --- /dev/null +++ b/include/vde/core/point.h @@ -0,0 +1,21 @@ +#pragma once +#include "vde/foundation/math_types.h" +#include "vde/foundation/tolerance.h" + +namespace vde::core { + +template +using Point = foundation::Point; + +using Point2D = foundation::Point2D; +using Point3D = foundation::Point3D; +using Point2f = foundation::Point2f; +using Point3f = foundation::Point3f; + +template +using Vector = foundation::Vector; + +using Vector2D = foundation::Vector2D; +using Vector3D = foundation::Vector3D; + +} // namespace vde::core diff --git a/include/vde/core/polygon.h b/include/vde/core/polygon.h new file mode 100644 index 0000000..f713c05 --- /dev/null +++ b/include/vde/core/polygon.h @@ -0,0 +1,32 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::core { + +class Polygon2D { +public: + Polygon2D() = default; + explicit Polygon2D(std::vector vertices) : vertices_(std::move(vertices)) {} + + [[nodiscard]] const std::vector& vertices() const { return vertices_; } + [[nodiscard]] size_t size() const { return vertices_.size(); } + [[nodiscard]] bool empty() const { return vertices_.empty(); } + + /// Signed area (positive = CCW) + [[nodiscard]] double signed_area() const; + + /// Absolute area + [[nodiscard]] double area() const { return std::abs(signed_area()); } + + /// Point-in-polygon test (ray casting) + [[nodiscard]] bool contains(const Point2D& p) const; + + /// Is the polygon counter-clockwise? + [[nodiscard]] bool is_ccw() const { return signed_area() > 0; } + +private: + std::vector vertices_; +}; + +} // namespace vde::core diff --git a/include/vde/core/transform.h b/include/vde/core/transform.h new file mode 100644 index 0000000..6555eca --- /dev/null +++ b/include/vde/core/transform.h @@ -0,0 +1,26 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::core { + +using Transform3D = Eigen::Transform; + +inline Transform3D translate(const Vector3D& v) { + return Transform3D(Eigen::Translation(v)); +} +inline Transform3D translate(double x, double y, double z) { + return translate(Vector3D(x, y, z)); +} +inline Transform3D rotate(const Vector3D& axis, double angle_rad) { + return Transform3D(Eigen::AngleAxis(angle_rad, axis.normalized())); +} +inline Transform3D rotate_x(double rad) { return rotate(Vector3D::UnitX(), rad); } +inline Transform3D rotate_y(double rad) { return rotate(Vector3D::UnitY(), rad); } +inline Transform3D rotate_z(double rad) { return rotate(Vector3D::UnitZ(), rad); } +inline Transform3D scale(double sx, double sy, double sz) { + return Transform3D(Eigen::Scaling(sx, sy, sz)); +} +inline Transform3D scale(double s) { return scale(s, s, s); } + +} // namespace vde::core diff --git a/include/vde/core/triangle.h b/include/vde/core/triangle.h new file mode 100644 index 0000000..dbb4f86 --- /dev/null +++ b/include/vde/core/triangle.h @@ -0,0 +1,42 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::core { + +template +class Triangle { +public: + Triangle() = default; + Triangle(const Point3D& v0, const Point3D& v1, const Point3D& v2) + : v_{v0, v1, v2} {} + + [[nodiscard]] const Point3D& v(int i) const { return v_[i]; } + [[nodiscard]] const std::array& vertices() const { return v_; } + [[nodiscard]] Vector3D normal() const { + return (v_[1] - v_[0]).cross(v_[2] - v_[0]).normalized(); + } + [[nodiscard]] T area() const { + return (v_[1] - v_[0]).cross(v_[2] - v_[0]).norm() * 0.5; + } + [[nodiscard]] Point3D centroid() const { + return (v_[0] + v_[1] + v_[2]) / 3.0; + } + [[nodiscard]] bool contains(const Point3D& p) const { + Vector3D v0 = v_[2] - v_[0], v1 = v_[1] - v_[0], v2 = p - v_[0]; + T d00 = v0.dot(v0), d01 = v0.dot(v1), d11 = v1.dot(v1); + T d20 = v2.dot(v0), d21 = v2.dot(v1); + T denom = d00 * d11 - d01 * d01; + T v = (d11 * d20 - d01 * d21) / denom; + T w = (d00 * d21 - d01 * d20) / denom; + return v >= 0 && w >= 0 && v + w <= 1; + } + +private: + std::array v_{}; +}; + +using Triangle3D = Triangle; +using Triangle3f = Triangle; + +} // namespace vde::core diff --git a/include/vde/curves/bezier_curve.h b/include/vde/curves/bezier_curve.h new file mode 100644 index 0000000..dc8f48c --- /dev/null +++ b/include/vde/curves/bezier_curve.h @@ -0,0 +1,27 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::curves { + +class BezierCurve { +public: + explicit BezierCurve(std::vector control_points); + + [[nodiscard]] Point3D evaluate(double t) const; + [[nodiscard]] Vector3D derivative(double t, int order = 1) const; + [[nodiscard]] int degree() const { return static_cast(cp_.size()) - 1; } + [[nodiscard]] std::pair domain() const { return {0.0, 1.0}; } + [[nodiscard]] const std::vector& control_points() const { return cp_; } + + /// Split at parameter t into two curves + [[nodiscard]] std::pair split(double t) const; + + /// Degree elevation + [[nodiscard]] BezierCurve degree_elevate(int times = 1) const; + +private: + std::vector cp_; +}; + +} // namespace vde::curves diff --git a/include/vde/curves/bezier_surface.h b/include/vde/curves/bezier_surface.h new file mode 100644 index 0000000..dc56093 --- /dev/null +++ b/include/vde/curves/bezier_surface.h @@ -0,0 +1,24 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::curves { + +class BezierSurface { +public: + BezierSurface(std::vector> control_grid); + + [[nodiscard]] Point3D evaluate(double u, double v) const; + [[nodiscard]] Vector3D derivative_u(double u, double v) const; + [[nodiscard]] Vector3D derivative_v(double u, double v) const; + [[nodiscard]] Vector3D normal(double u, double v) const; + + [[nodiscard]] int degree_u() const { return static_cast(cp_.size()) - 1; } + [[nodiscard]] int degree_v() const { return static_cast(cp_[0].size()) - 1; } + +private: + std::vector> cp_; + [[nodiscard]] Point3D de_casteljau(double t, const std::vector& pts) const; +}; + +} // namespace vde::curves diff --git a/include/vde/curves/bspline_curve.h b/include/vde/curves/bspline_curve.h new file mode 100644 index 0000000..a53a4fe --- /dev/null +++ b/include/vde/curves/bspline_curve.h @@ -0,0 +1,31 @@ +#pragma once +#include "vde/core/point.h" +#include + +namespace vde::curves { + +class BSplineCurve { +public: + BSplineCurve(std::vector control_points, + std::vector knots, int degree); + + [[nodiscard]] Point3D evaluate(double t) const; + [[nodiscard]] Vector3D derivative(double t, int order = 1) const; + [[nodiscard]] int degree() const { return degree_; } + [[nodiscard]] std::pair domain() const; + [[nodiscard]] const std::vector& control_points() const { return cp_; } + [[nodiscard]] const std::vector& knots() const { return knots_; } + + /// Find knot span index for t + [[nodiscard]] int find_span(double t) const; + + /// Evaluate basis functions at t + [[nodiscard]] std::vector basis_functions(double t, int span = -1) const; + +private: + std::vector cp_; + std::vector knots_; + int degree_; +}; + +} // namespace vde::curves diff --git a/include/vde/curves/bspline_surface.h b/include/vde/curves/bspline_surface.h new file mode 100644 index 0000000..25e1949 --- /dev/null +++ b/include/vde/curves/bspline_surface.h @@ -0,0 +1,25 @@ +#pragma once +#include "vde/curves/bspline_curve.h" +#include + +namespace vde::curves { + +class BSplineSurface { +public: + BSplineSurface(std::vector> control_grid, + std::vector knots_u, std::vector knots_v, + int degree_u, int degree_v); + + [[nodiscard]] Point3D evaluate(double u, double v) const; + [[nodiscard]] int degree_u() const { return degree_u_; } + [[nodiscard]] int degree_v() const { return degree_v_; } + [[nodiscard]] const std::vector& knots_u() const { return knots_u_; } + [[nodiscard]] const std::vector& knots_v() const { return knots_v_; } + +private: + std::vector> cp_; + std::vector knots_u_, knots_v_; + int degree_u_, degree_v_; +}; + +} // namespace vde::curves diff --git a/include/vde/curves/nurbs_curve.h b/include/vde/curves/nurbs_curve.h new file mode 100644 index 0000000..83d6163 --- /dev/null +++ b/include/vde/curves/nurbs_curve.h @@ -0,0 +1,27 @@ +#pragma once +#include "vde/curves/bspline_curve.h" +#include + +namespace vde::curves { + +class NurbsCurve { +public: + NurbsCurve(std::vector control_points, std::vector knots, + std::vector weights, int degree); + + [[nodiscard]] Point3D evaluate(double t) const; + [[nodiscard]] Vector3D derivative(double t, int order = 1) const; + [[nodiscard]] int degree() const { return degree_; } + [[nodiscard]] std::pair domain() const; + [[nodiscard]] const std::vector& control_points() const { return cp_; } + [[nodiscard]] const std::vector& weights() const { return weights_; } + [[nodiscard]] const std::vector& knots() const { return knots_; } + +private: + std::vector cp_; + std::vector knots_; + std::vector weights_; + int degree_; +}; + +} // namespace vde::curves diff --git a/include/vde/curves/tessellation.h b/include/vde/curves/tessellation.h new file mode 100644 index 0000000..d78c4d4 --- /dev/null +++ b/include/vde/curves/tessellation.h @@ -0,0 +1,16 @@ +#pragma once +#include "vde/core/point.h" +#include +#include + +namespace vde::curves { + +/// Tessellate a parametric surface into a triangle mesh +/// @param eval Function(u,v) -> Point3D +/// @param res_u, res_v Number of samples in u/v directions +/// @return Pairs of (vertices, triangle indices) +std::pair, std::vector>> +tessellate(const std::function& eval, + int res_u, int res_v); + +} // namespace vde::curves diff --git a/include/vde/foundation/error_codes.h b/include/vde/foundation/error_codes.h new file mode 100644 index 0000000..6446e62 --- /dev/null +++ b/include/vde/foundation/error_codes.h @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace vde::foundation { + +enum class ErrorCode : int32_t { + kSuccess = 0, + kInvalidArgument, + kInvalidGeometry, + kDegenerateCase, + kNotImplemented, + kNumericalError, + kOutOfMemory, + kInternalError, +}; + +} // namespace vde::foundation diff --git a/include/vde/foundation/exact_predicates.h b/include/vde/foundation/exact_predicates.h new file mode 100644 index 0000000..2219568 --- /dev/null +++ b/include/vde/foundation/exact_predicates.h @@ -0,0 +1,20 @@ +#pragma once +#include "vde/foundation/math_types.h" + +namespace vde::foundation::exact { + +enum class Orientation { Clockwise, CounterClockwise, Collinear }; +enum class CircleTest { Inside, On, Outside }; + +/// Adaptive-precision 2D orientation test (Shewchuk) +Orientation orient_2d(const Point2D& a, const Point2D& b, const Point2D& c); + +/// Adaptive-precision 3D orientation test +Orientation orient_3d(const Point3D& a, const Point3D& b, + const Point3D& c, const Point3D& d); + +/// Adaptive-precision in-circle test +CircleTest in_circle(const Point2D& a, const Point2D& b, + const Point2D& c, const Point2D& d); + +} // namespace vde::foundation::exact diff --git a/include/vde/foundation/io_obj.h b/include/vde/foundation/io_obj.h new file mode 100644 index 0000000..260499f --- /dev/null +++ b/include/vde/foundation/io_obj.h @@ -0,0 +1,17 @@ +#pragma once +#include +#include +#include "vde/foundation/math_types.h" + +namespace vde::foundation { + +struct ObjMeshData { + std::vector vertices; + std::vector normals; + std::vector> faces; // vertex indices (1-based in file, 0-based here) +}; + +ObjMeshData read_obj(const std::string& filepath); +void write_obj(const std::string& filepath, const ObjMeshData& data); + +} // namespace vde::foundation diff --git a/include/vde/foundation/io_stl.h b/include/vde/foundation/io_stl.h new file mode 100644 index 0000000..2274085 --- /dev/null +++ b/include/vde/foundation/io_stl.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +#include "vde/foundation/math_types.h" + +namespace vde::foundation { + +struct StlTriangle { + Vector3D normal; + Point3D v0, v1, v2; +}; + +std::vector read_stl(const std::string& filepath); +void write_stl(const std::string& filepath, const std::vector& tris); + +} // namespace vde::foundation diff --git a/include/vde/foundation/math_types.h b/include/vde/foundation/math_types.h new file mode 100644 index 0000000..c1cbb35 --- /dev/null +++ b/include/vde/foundation/math_types.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include + +namespace vde::foundation { + +template +using Point = Eigen::Matrix; + +template +using Vector = Eigen::Matrix; + +template +using Matrix3 = Eigen::Matrix; + +template +using Matrix4 = Eigen::Matrix; + +using Point2D = Point; +using Point3D = Point; +using Point2f = Point; +using Point3f = Point; + +using Vector2D = Vector; +using Vector3D = Vector; +using Vector2f = Vector; +using Vector3f = Vector; + +using Matrix3D = Matrix3; +using Matrix4D = Matrix4; + +} // namespace vde::foundation diff --git a/include/vde/foundation/memory_pool.h b/include/vde/foundation/memory_pool.h new file mode 100644 index 0000000..40f271a --- /dev/null +++ b/include/vde/foundation/memory_pool.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include + +namespace vde::foundation { + +template +class MemoryPool { +public: + explicit MemoryPool(size_t chunk_size = 1024) : chunk_size_(chunk_size) {} + ~MemoryPool() { for (auto* p : chunks_) ::operator delete(p); } + + T* allocate(size_t n = 1) { + if (free_list_ == nullptr) allocate_chunk(); + T* p = static_cast(free_list_); + free_list_ = *reinterpret_cast(free_list_); + return p; + } + + void deallocate(T* p, size_t = 1) { + *reinterpret_cast(p) = free_list_; + free_list_ = p; + } + +private: + void allocate_chunk() { + void* chunk = ::operator new(chunk_size_ * sizeof(T)); + chunks_.push_back(chunk); + char* base = static_cast(chunk); + for (size_t i = 0; i < chunk_size_; ++i) { + void* ptr = base + i * sizeof(T); + *reinterpret_cast(ptr) = free_list_; + free_list_ = ptr; + } + } + + size_t chunk_size_; + void* free_list_ = nullptr; + std::vector chunks_; +}; + +} // namespace vde::foundation diff --git a/include/vde/foundation/tolerance.h b/include/vde/foundation/tolerance.h new file mode 100644 index 0000000..df6e409 --- /dev/null +++ b/include/vde/foundation/tolerance.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include + +namespace vde::foundation { + +class Tolerance { +public: + Tolerance(double absolute = 1e-6, double relative = 1e-8, + double angular = 1e-8, double snapping = 1e-4) + : absolute_(absolute), relative_(relative), + angular_(angular), snapping_(snapping) {} + + /// Two points are coincident + template + bool points_equal(const Eigen::Matrix& a, + const Eigen::Matrix& b) const { + return (a - b).norm() < absolute_; + } + + /// Value is effectively zero + template + bool is_zero(T value) const { + return std::abs(value) < absolute_; + } + + /// Two values are equal within tolerance + template + bool equals(T a, T b) const { + return std::abs(a - b) < absolute_ + relative_ * std::max(std::abs(a), std::abs(b)); + } + + double absolute() const { return absolute_; } + double relative() const { return relative_; } + double angular() const { return angular_; } + double snapping() const { return snapping_; } + + void set_absolute(double v) { absolute_ = v; } + void set_relative(double v) { relative_ = v; } + + static Tolerance& global() { return global_; } + static void set_global(const Tolerance& tol) { global_ = tol; } + +private: + double absolute_; + double relative_; + double angular_; + double snapping_; + static Tolerance global_; +}; + +} // namespace vde::foundation diff --git a/include/vde/mesh/delaunay_2d.h b/include/vde/mesh/delaunay_2d.h new file mode 100644 index 0000000..09f831b --- /dev/null +++ b/include/vde/mesh/delaunay_2d.h @@ -0,0 +1,16 @@ +#pragma once +#include "vde/core/point.h" +#include +#include + +namespace vde::mesh { + +struct DelaunayResult { + std::vector vertices; + std::vector> triangles; // indices into vertices +}; + +/// Bowyer-Watson incremental Delaunay triangulation, O(n²) worst, O(n log n) typical +DelaunayResult delaunay_2d(const std::vector& points); + +} // namespace vde::mesh diff --git a/include/vde/mesh/halfedge_mesh.h b/include/vde/mesh/halfedge_mesh.h new file mode 100644 index 0000000..1631ba1 --- /dev/null +++ b/include/vde/mesh/halfedge_mesh.h @@ -0,0 +1,73 @@ +#pragma once +#include "vde/core/point.h" +#include "vde/core/aabb.h" +#include +#include + +namespace vde::mesh { + +struct Halfedge { + int vertex_index = -1; + int face_index = -1; + int next_index = -1; + int prev_index = -1; + int opposite_index = -1; +}; + +struct Face { + int halfedge_index = -1; + int valence = 3; +}; + +class HalfedgeMesh { +public: + HalfedgeMesh() = default; + + // ── Construction ── + void clear(); + int add_vertex(const Point3D& p); + int add_face(const std::vector& vertex_indices); + void build_from_triangles(const std::vector& verts, + const std::vector>& tris); + + // ── Accessors ── + [[nodiscard]] size_t num_vertices() const { return vertices_.size(); } + [[nodiscard]] size_t num_faces() const { return faces_.size(); } + [[nodiscard]] size_t num_edges() const { return halfedges_.size() / 2; } + [[nodiscard]] const Point3D& vertex(size_t idx) const { return vertices_[idx]; } + [[nodiscard]] const Face& face(size_t idx) const { return faces_[idx]; } + [[nodiscard]] const Halfedge& halfedge(size_t idx) const { return halfedges_[idx]; } + void set_vertex(size_t idx, const Point3D& p) { vertices_[idx] = p; normals_dirty_ = true; } + + // ── Topology queries ── + [[nodiscard]] std::vector face_vertices(int fi) const; + [[nodiscard]] std::vector vertex_faces(int vi) const; + [[nodiscard]] bool is_boundary_edge(int hei) const { return halfedges_[hei].face_index < 0; } + [[nodiscard]] bool is_boundary_vertex(int vi) const; + + // ── Normals ── + [[nodiscard]] Vector3D face_normal(int fi) const; + [[nodiscard]] Vector3D vertex_normal(int vi) const; + void update_normals(); + + // ── Bounds ── + [[nodiscard]] AABB3D bounds() const; + + // ── Iterators for range-based for ── + class FaceRange; + class VertexOneRing; + [[nodiscard]] FaceRange faces_range() const; + [[nodiscard]] VertexOneRing vertex_one_ring(int vi) const; + +private: + std::vector vertices_; + std::vector halfedges_; + std::vector faces_; + std::vector face_normals_; + std::vector vertex_normals_; + bool normals_dirty_ = true; + + int find_or_create_edge(int v0, int v1); +}; + +} // namespace vde::mesh diff --git a/include/vde/mesh/mesh_boolean.h b/include/vde/mesh/mesh_boolean.h new file mode 100644 index 0000000..ec26b83 --- /dev/null +++ b/include/vde/mesh/mesh_boolean.h @@ -0,0 +1,10 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" + +namespace vde::mesh { + +enum class BooleanOp { Union, Intersection, Difference, SymDiff }; + +HalfedgeMesh mesh_boolean(const HalfedgeMesh& a, const HalfedgeMesh& b, BooleanOp op); + +} // namespace vde::mesh diff --git a/include/vde/mesh/mesh_quality.h b/include/vde/mesh/mesh_quality.h new file mode 100644 index 0000000..7d2145b --- /dev/null +++ b/include/vde/mesh/mesh_quality.h @@ -0,0 +1,17 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" +#include + +namespace vde::mesh { + +struct MeshQuality { + double min_angle_deg = 0; + double max_angle_deg = 0; + double avg_aspect_ratio = 0; + double max_aspect_ratio = 0; + size_t degenerate_faces = 0; +}; + +MeshQuality evaluate_mesh_quality(const HalfedgeMesh& mesh); + +} // namespace vde::mesh diff --git a/include/vde/mesh/mesh_repair.h b/include/vde/mesh/mesh_repair.h new file mode 100644 index 0000000..1ae26ef --- /dev/null +++ b/include/vde/mesh/mesh_repair.h @@ -0,0 +1,15 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" + +namespace vde::mesh { + +struct RepairOptions { + bool fill_holes = true; + bool remove_duplicates = true; + bool fix_orientation = true; + bool remove_degenerate = true; +}; + +HalfedgeMesh repair_mesh(const HalfedgeMesh& mesh, const RepairOptions& opts = {}); + +} // namespace vde::mesh diff --git a/include/vde/mesh/mesh_simplify.h b/include/vde/mesh/mesh_simplify.h new file mode 100644 index 0000000..fd1dc4e --- /dev/null +++ b/include/vde/mesh/mesh_simplify.h @@ -0,0 +1,16 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" + +namespace vde::mesh { + +struct SimplifyOptions { + double target_ratio = 0.5; // Target face ratio (0, 1) + int max_iterations = 100; + bool preserve_boundary = true; +}; + +/// QEM (Quadric Error Metrics) mesh simplification +/// Returns simplified mesh with ~target_ratio * original face count +HalfedgeMesh simplify_mesh(const HalfedgeMesh& mesh, const SimplifyOptions& opts = {}); + +} // namespace vde::mesh diff --git a/include/vde/mesh/mesh_smooth.h b/include/vde/mesh/mesh_smooth.h new file mode 100644 index 0000000..42a8e9e --- /dev/null +++ b/include/vde/mesh/mesh_smooth.h @@ -0,0 +1,17 @@ +#pragma once +#include "vde/mesh/halfedge_mesh.h" + +namespace vde::mesh { + +enum class SmoothMethod { Laplacian, Taubin }; + +struct SmoothOptions { + int iterations = 10; + double lambda = 0.5; // Laplacian weight + double mu = -0.53; // Taubin shrink (mu < -lambda for Taubin) + SmoothMethod method = SmoothMethod::Laplacian; +}; + +HalfedgeMesh smooth_mesh(const HalfedgeMesh& mesh, const SmoothOptions& opts = {}); + +} // namespace vde::mesh diff --git a/include/vde/spatial/bvh.h b/include/vde/spatial/bvh.h new file mode 100644 index 0000000..5534fe0 --- /dev/null +++ b/include/vde/spatial/bvh.h @@ -0,0 +1,48 @@ +#pragma once +#include "vde/spatial/spatial_index.h" +#include "vde/core/aabb.h" +#include "vde/core/triangle.h" + +namespace vde::spatial { + +enum class BVHSplitStrategy { Middle, Equal, SAH }; + +struct BVHBuildOptions { + BVHSplitStrategy strategy = BVHSplitStrategy::SAH; + int leaf_size = 4; + int max_depth = 64; +}; + +class BVH : public SpatialIndex { +public: + explicit BVH(const BVHBuildOptions& opts = {}) : opts_(opts) {} + + void build(const std::vector& tris) override; + void insert(const Triangle3D& tri) override; + bool remove(const Triangle3D& tri) override; + std::vector query_range(const AABB3D& range) const override; + std::vector query_knn(const Point3D& point, size_t k) const override; + std::vector query_ray(const Ray3Dd& ray) const override; + void clear() override; + size_t size() const override { return primitives_.size(); } + + /// Find closest hit along ray + struct HitResult { double t; Triangle3D tri; Point3D point; }; + std::optional query_ray_nearest(const Ray3Dd& ray) const; + +private: + struct Node { + AABB3D bounds; + int left = -1, right = -1; + int first_prim = 0, prim_count = 0; + bool leaf() const { return left < 0; } + }; + std::vector nodes_; + std::vector primitives_; + BVHBuildOptions opts_; + + void build_recursive(int node, int first, int count, int depth); + double sah_cost(int n_left, int n_right, const AABB3D& left, const AABB3D& right) const; +}; + +} // namespace vde::spatial diff --git a/include/vde/spatial/kd_tree.h b/include/vde/spatial/kd_tree.h new file mode 100644 index 0000000..c2c7765 --- /dev/null +++ b/include/vde/spatial/kd_tree.h @@ -0,0 +1,21 @@ +#pragma once +#include "vde/spatial/spatial_index.h" + +namespace vde::spatial { + +template +class KDTree : public SpatialIndex { +public: + void build(const std::vector& items) override; + void insert(const T& item) override; + bool remove(const T& item) override; + std::vector query_range(const AABB3D& range) const override; + std::vector query_knn(const Point3D& point, size_t k) const override; + std::vector query_ray(const Ray3Dd& ray) const override; + void clear() override; + size_t size() const override { return items_.size(); } +private: + std::vector items_; +}; + +} // namespace vde::spatial diff --git a/include/vde/spatial/octree.h b/include/vde/spatial/octree.h new file mode 100644 index 0000000..1344554 --- /dev/null +++ b/include/vde/spatial/octree.h @@ -0,0 +1,24 @@ +#pragma once +#include "vde/spatial/spatial_index.h" + +namespace vde::spatial { + +template +class Octree : public SpatialIndex { +public: + explicit Octree(int max_depth = 8, int max_items = 16) + : max_depth_(max_depth), max_items_(max_items) {} + void build(const std::vector& items) override; + void insert(const T& item) override; + bool remove(const T& item) override; + std::vector query_range(const AABB3D& range) const override; + std::vector query_knn(const Point3D& point, size_t k) const override; + std::vector query_ray(const Ray3Dd& ray) const override; + void clear() override; + size_t size() const override { return count_; } +private: + int max_depth_, max_items_; + size_t count_ = 0; +}; + +} // namespace vde::spatial diff --git a/include/vde/spatial/r_tree.h b/include/vde/spatial/r_tree.h new file mode 100644 index 0000000..f785410 --- /dev/null +++ b/include/vde/spatial/r_tree.h @@ -0,0 +1,21 @@ +#pragma once +#include "vde/spatial/spatial_index.h" + +namespace vde::spatial { + +template +class RTree : public SpatialIndex { +public: + void build(const std::vector& items) override; + void insert(const T& item) override; + bool remove(const T& item) override; + std::vector query_range(const AABB3D& range) const override; + std::vector query_knn(const Point3D& point, size_t k) const override; + std::vector query_ray(const Ray3Dd& ray) const override; + void clear() override; + size_t size() const override { return count_; } +private: + size_t count_ = 0; +}; + +} // namespace vde::spatial diff --git a/include/vde/spatial/spatial_index.h b/include/vde/spatial/spatial_index.h new file mode 100644 index 0000000..2cca759 --- /dev/null +++ b/include/vde/spatial/spatial_index.h @@ -0,0 +1,23 @@ +#pragma once +#include "vde/core/aabb.h" +#include "vde/core/line.h" +#include +#include + +namespace vde::spatial { + +template +class SpatialIndex { +public: + virtual ~SpatialIndex() = default; + virtual void build(const std::vector& items) = 0; + virtual void insert(const T& item) = 0; + virtual bool remove(const T& item) = 0; + virtual std::vector query_range(const AABB3D& range) const = 0; + virtual std::vector query_knn(const Point3D& point, size_t k) const = 0; + virtual std::vector query_ray(const Ray3Dd& ray) const = 0; + virtual void clear() = 0; + virtual size_t size() const = 0; +}; + +} // namespace vde::spatial diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..8d2bd85 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,125 @@ +# ── 编译选项接口目标 ────────────────────────────── +add_library(vde_compile_options INTERFACE) +include(${CMAKE_SOURCE_DIR}/cmake/CompilerSettings.cmake) + +# ── foundation ───────────────────────────────────── +add_library(vde_foundation STATIC + foundation/tolerance.cpp + foundation/exact_predicates.cpp + foundation/io_obj.cpp + foundation/io_stl.cpp +) +target_include_directories(vde_foundation + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_foundation + PUBLIC vde_compile_options Eigen3::Eigen +) + +# ── core ─────────────────────────────────────────── +add_library(vde_core STATIC + core/point.cpp + core/transform.cpp + core/polygon.cpp + core/distance.cpp + core/convex_hull.cpp +) +target_include_directories(vde_core + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_core + PUBLIC vde_foundation vde_compile_options +) + +# ── curves ───────────────────────────────────────── +add_library(vde_curves STATIC + curves/bezier_curve.cpp + curves/bspline_curve.cpp + curves/nurbs_curve.cpp + curves/bezier_surface.cpp + curves/bspline_surface.cpp + curves/tessellation.cpp +) +target_include_directories(vde_curves + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_curves + PUBLIC vde_core vde_compile_options +) + +# ── mesh ──────────────────────────────────────────── +add_library(vde_mesh STATIC + mesh/halfedge_mesh.cpp + mesh/delaunay_2d.cpp + mesh/mesh_simplify.cpp + mesh/mesh_smooth.cpp + mesh/mesh_boolean.cpp + mesh/mesh_quality.cpp + mesh/mesh_repair.cpp +) +target_include_directories(vde_mesh + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_mesh + PUBLIC vde_core vde_compile_options +) + +# ── spatial ───────────────────────────────────────── +add_library(vde_spatial STATIC + spatial/bvh.cpp + spatial/octree.cpp + spatial/kd_tree.cpp + spatial/r_tree.cpp +) +target_include_directories(vde_spatial + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_spatial + PUBLIC vde_core vde_compile_options +) + +# ── boolean ───────────────────────────────────────── +add_library(vde_boolean STATIC + boolean/boolean_2d.cpp + boolean/boolean_mesh.cpp +) +target_include_directories(vde_boolean + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_boolean + PUBLIC vde_mesh vde_spatial vde_compile_options +) + +# ── collision ─────────────────────────────────────── +add_library(vde_collision STATIC + collision/gjk.cpp + collision/sat.cpp + collision/ray_intersect.cpp + collision/tri_intersect.cpp +) +target_include_directories(vde_collision + PUBLIC ${CMAKE_SOURCE_DIR}/include + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} +) +target_link_libraries(vde_collision + PUBLIC vde_core vde_spatial vde_compile_options +) + +# ── 聚合库 ────────────────────────────────────────── +add_library(vde ${CMAKE_BUILD_TYPE_LIB}) +target_link_libraries(vde + PUBLIC vde_foundation + PUBLIC vde_core + PUBLIC vde_curves + PUBLIC vde_mesh + PUBLIC vde_spatial + PUBLIC vde_boolean + PUBLIC vde_collision +) +add_library(vde::engine ALIAS vde) diff --git a/src/boolean/boolean_2d.cpp b/src/boolean/boolean_2d.cpp new file mode 100644 index 0000000..901cedb --- /dev/null +++ b/src/boolean/boolean_2d.cpp @@ -0,0 +1,65 @@ +#include "vde/boolean/boolean_2d.h" +#include + +namespace vde::boolean { + +// Simple implementation for convex polygons using Sutherland-Hodgman approach +static Polygon2D clip_polygon(const Polygon2D& subject, const Polygon2D& clip) { + auto result = subject; + const auto& cv = clip.vertices(); + int n = static_cast(cv.size()); + + for (int i = 0; i < n; ++i) { + if (result.empty()) break; + auto input = std::move(result); + result = Polygon2D(); + const Point2D& e0 = cv[i]; + const Point2D& e1 = cv[(i + 1) % n]; + + for (size_t j = 0; j < input.size(); ++j) { + const Point2D& p0 = input.vertices()[j]; + const Point2D& p1 = input.vertices()[(j + 1) % input.size()]; + + Vector2D edge = e1 - e0; + double d0 = (p0.x() - e0.x()) * edge.y() - (p0.y() - e0.y()) * edge.x(); + double d1 = (p1.x() - e0.x()) * edge.y() - (p1.y() - e0.y()) * edge.x(); + + bool in0 = d0 <= 0; + bool in1 = d1 <= 0; + + if (in0 && in1) { + result = Polygon2D(result.vertices()); + const_cast&>(result.vertices()).push_back(p1); + } else if (in0 && !in1) { + double t = d0 / (d0 - d1); + Point2D inter(p0.x() + t * (p1.x() - p0.x()), + p0.y() + t * (p1.y() - p0.y())); + auto verts = result.vertices(); + verts.push_back(inter); + result = Polygon2D(verts); + } else if (!in0 && in1) { + double t = d0 / (d0 - d1); + Point2D inter(p0.x() + t * (p1.x() - p0.x()), + p0.y() + t * (p1.y() - p0.y())); + auto verts = result.vertices(); + verts.push_back(inter); + verts.push_back(p1); + result = Polygon2D(verts); + } + } + } + return result; +} + +std::vector boolean_2d(const Polygon2D& a, const Polygon2D& b, BooleanOp op) { + // Assume convex polygons for this simplified implementation + if (op == BooleanOp::Intersection) { + auto result = clip_polygon(a, b); + if (result.empty()) return {}; + return {result}; + } + // Union, Difference, SymDiff not implemented yet + return {a}; +} + +} // namespace vde::boolean diff --git a/src/boolean/boolean_mesh.cpp b/src/boolean/boolean_mesh.cpp new file mode 100644 index 0000000..e68fcfc --- /dev/null +++ b/src/boolean/boolean_mesh.cpp @@ -0,0 +1,6 @@ +#include "vde/boolean/boolean_mesh.h" +namespace vde::boolean { +HalfedgeMesh mesh_boolean(const HalfedgeMesh&, const HalfedgeMesh&, BooleanOp) { + return {}; +} +} diff --git a/src/collision/gjk.cpp b/src/collision/gjk.cpp new file mode 100644 index 0000000..37b5645 --- /dev/null +++ b/src/collision/gjk.cpp @@ -0,0 +1,96 @@ +#include "vde/collision/gjk.h" +#include +#include + +namespace vde::collision { + +namespace { + Point3D support(const SupportFunc& shape_a, const SupportFunc& shape_b, const Vector3D& d) { + return shape_a(d) - shape_b(-d); + } +} + +bool gjk_intersect(const SupportFunc& shape_a, const SupportFunc& shape_b) { + // Initial direction + Vector3D d = Vector3D(1, 0, 0); + std::array simplex; + simplex[0] = support(shape_a, shape_b, d); + d = -simplex[0]; + + int count = 1; + constexpr int MAX_ITER = 64; + + for (int iter = 0; iter < MAX_ITER; ++iter) { + Point3D p = support(shape_a, shape_b, d); + if (p.dot(d) < 0) return false; + simplex[count++] = p; + + // Check if origin is in simplex (simplified: 2D/3D) + if (count == 2) { + // Line case + Vector3D ab = simplex[1] - simplex[0]; + Vector3D ao = -simplex[0]; + if (ab.dot(ao) > 0) { + d = ab.cross(ao).cross(ab); + } else { + simplex[0] = simplex[1]; + d = ao; + count = 1; + } + } else if (count == 3) { + // Triangle case + Vector3D a = simplex[2], b = simplex[1], c = simplex[0]; + Vector3D ao = -a; + Vector3D ab = b - a, ac = c - a; + Vector3D abc = ab.cross(ac); + if (abc.cross(ac).dot(ao) > 0) { + if (ac.dot(ao) > 0) { + simplex[1] = simplex[2]; + d = ac.cross(ao).cross(ac); + } else { + if (ab.dot(ao) > 0) { + simplex[0] = simplex[1]; + simplex[1] = simplex[2]; + d = ab.cross(ao).cross(ab); + } else { + simplex[0] = simplex[2]; + d = ao; + } + } + count = 2; + } else if (ab.cross(abc).dot(ao) > 0) { + simplex[0] = simplex[1]; + simplex[1] = simplex[2]; + count = 2; + if (ab.dot(ao) > 0) d = ab.cross(ao).cross(ab); + else { simplex[0] = simplex[2]; d = ao; } + } else { + if (abc.dot(ao) > 0) { + simplex[0] = simplex[1]; + simplex[1] = simplex[2]; + d = abc; + count = 3; + } else { + // Swap for opposite face + simplex[0] = simplex[1]; + simplex[1] = simplex[2]; + d = -abc; + count = 3; + } + } + } else if (count == 4) { + return true; // Origin enclosed in tetrahedron + } + } + return false; +} + +double gjk_distance(const SupportFunc& a, const SupportFunc& b) { + return gjk_intersect(a, b) ? 0.0 : 1.0; // Simplified +} + +GJKResult gjk_full(const SupportFunc& a, const SupportFunc& b) { + return {gjk_intersect(a, b), 0.0, {}, {}}; +} + +} // namespace vde::collision diff --git a/src/collision/ray_intersect.cpp b/src/collision/ray_intersect.cpp new file mode 100644 index 0000000..a6a06ea --- /dev/null +++ b/src/collision/ray_intersect.cpp @@ -0,0 +1,26 @@ +#include "vde/collision/ray_intersect.h" +#include + +namespace vde::collision { + +std::optional ray_triangle_intersect( + const Point3D& origin, const Vector3D& dir, const Triangle3D& tri) { + const double EPS = 1e-8; + Vector3D e1 = tri.v(1) - tri.v(0); + Vector3D e2 = tri.v(2) - tri.v(0); + Vector3D pvec = dir.cross(e2); + double det = e1.dot(pvec); + if (std::abs(det) < EPS) return std::nullopt; + double inv_det = 1.0 / det; + Vector3D tvec = origin - tri.v(0); + double u = tvec.dot(pvec) * inv_det; + if (u < 0 || u > 1) return std::nullopt; + Vector3D qvec = tvec.cross(e1); + double v = dir.dot(qvec) * inv_det; + if (v < 0 || u + v > 1) return std::nullopt; + double t = e2.dot(qvec) * inv_det; + if (t < EPS) return std::nullopt; + return RayTriResult{t, u, v, origin + dir * t}; +} + +} // namespace vde::collision diff --git a/src/collision/sat.cpp b/src/collision/sat.cpp new file mode 100644 index 0000000..764eb8a --- /dev/null +++ b/src/collision/sat.cpp @@ -0,0 +1,7 @@ +#include "vde/collision/sat.h" +namespace vde::collision { +bool sat_intersect(const std::vector&, const std::vector>&, + const std::vector&, const std::vector>&) { + return false; +} +} diff --git a/src/collision/tri_intersect.cpp b/src/collision/tri_intersect.cpp new file mode 100644 index 0000000..56a2273 --- /dev/null +++ b/src/collision/tri_intersect.cpp @@ -0,0 +1,4 @@ +#include "vde/collision/tri_intersect.h" +namespace vde::collision { +bool tri_tri_intersect(const Triangle3D&, const Triangle3D&) { return false; } +} diff --git a/src/core/convex_hull.cpp b/src/core/convex_hull.cpp new file mode 100644 index 0000000..7453630 --- /dev/null +++ b/src/core/convex_hull.cpp @@ -0,0 +1,38 @@ +#include "vde/core/convex_hull.h" +#include +#include + +namespace vde::core { + +static double cross(const Point2D& o, const Point2D& a, const Point2D& b) { + return (a.x() - o.x()) * (b.y() - o.y()) - (a.y() - o.y()) * (b.x() - o.x()); +} + +std::vector convex_hull_2d(const std::vector& pts) { + if (pts.size() <= 2) return pts; + auto points = pts; + std::sort(points.begin(), points.end(), + [](const Point2D& a, const Point2D& b) { + return a.x() < b.x() || (a.x() == b.x() && a.y() < b.y()); + }); + std::vector hull(2 * points.size()); + int k = 0; + for (size_t i = 0; i < points.size(); ++i) { + while (k >= 2 && cross(hull[k-2], hull[k-1], points[i]) <= 0) --k; + hull[k++] = points[i]; + } + int lower_end = k + 1; + for (int i = static_cast(points.size()) - 2; i >= 0; --i) { + while (k >= lower_end && cross(hull[k-2], hull[k-1], points[i]) <= 0) --k; + hull[k++] = points[i]; + } + hull.resize(k - 1); + return hull; +} + +std::vector> convex_hull_3d(const std::vector&) { + // TODO: QuickHull implementation + return {}; +} + +} // namespace vde::core diff --git a/src/core/distance.cpp b/src/core/distance.cpp new file mode 100644 index 0000000..062736e --- /dev/null +++ b/src/core/distance.cpp @@ -0,0 +1,56 @@ +#include "vde/core/distance.h" +#include + +namespace vde::core { + +double distance(const Point3D& a, const Point3D& b) { return (a - b).norm(); } + +double distance(const Point3D& p, const Line3D& line) { + Vector3D v = p - line.origin(); + return (v - v.dot(line.direction()) * line.direction()).norm(); +} + +double distance(const Point3D& p, const Segment3D& seg) { + Vector3D ab = seg.p1() - seg.p0(); + Vector3D ap = p - seg.p0(); + double t = ap.dot(ab) / ab.squaredNorm(); + t = std::clamp(t, 0.0, 1.0); + return (seg.p0() + t * ab - p).norm(); +} + +double distance(const Point3D& p, const Plane& plane) { + return plane.distance(p); +} + +double distance(const Point3D& p, const Triangle& tri) { + return (p - closest_point(p, tri)).norm(); +} + +Point3D closest_point(const Point3D& p, const Triangle& tri) { + Vector3D v0 = tri.v(2) - tri.v(0), v1 = tri.v(1) - tri.v(0), v2 = p - tri.v(0); + double d00 = v0.dot(v0), d01 = v0.dot(v1), d11 = v1.dot(v1); + double d20 = v2.dot(v0), d21 = v2.dot(v1); + double denom = d00 * d11 - d01 * d01; + double v = (d11 * d20 - d01 * d21) / denom; + double w = (d00 * d21 - d01 * d20) / denom; + if (v >= 0 && w >= 0 && v + w <= 1) + return tri.v(0) + v * v0 + w * v1; + // Fallback to closest point on edges + double d0 = distance(p, Segment3D(tri.v(0), tri.v(1))); + double d1 = distance(p, Segment3D(tri.v(1), tri.v(2))); + double d2 = distance(p, Segment3D(tri.v(2), tri.v(0))); + double dmin = std::min({d0, d1, d2}); + if (dmin == d0) return closest_point(p, Segment3D(tri.v(0), tri.v(1))); + if (dmin == d1) return closest_point(p, Segment3D(tri.v(1), tri.v(2))); + return closest_point(p, Segment3D(tri.v(2), tri.v(0))); +} + +Point3D closest_point(const Point3D& p, const Segment3D& seg) { + Vector3D ab = seg.p1() - seg.p0(); + Vector3D ap = p - seg.p0(); + double t = ap.dot(ab) / ab.squaredNorm(); + t = std::clamp(t, 0.0, 1.0); + return seg.p0() + t * ab; +} + +} // namespace vde::core diff --git a/src/core/point.cpp b/src/core/point.cpp new file mode 100644 index 0000000..46bb60f --- /dev/null +++ b/src/core/point.cpp @@ -0,0 +1,2 @@ +#include "vde/core/point.h" +namespace vde::core {} diff --git a/src/core/polygon.cpp b/src/core/polygon.cpp new file mode 100644 index 0000000..8e3d07c --- /dev/null +++ b/src/core/polygon.cpp @@ -0,0 +1,32 @@ +#include "vde/core/polygon.h" +#include + +namespace vde::core { + +double Polygon2D::signed_area() const { + if (vertices_.size() < 3) return 0.0; + double area = 0.0; + for (size_t i = 0; i < vertices_.size(); ++i) { + const auto& a = vertices_[i]; + const auto& b = vertices_[(i + 1) % vertices_.size()]; + area += a.x() * b.y() - b.x() * a.y(); + } + return area * 0.5; +} + +bool Polygon2D::contains(const Point2D& p) const { + // Ray casting algorithm + bool inside = false; + size_t n = vertices_.size(); + for (size_t i = 0, j = n - 1; i < n; j = i++) { + const auto& vi = vertices_[i]; + const auto& vj = vertices_[j]; + if (((vi.y() > p.y()) != (vj.y() > p.y())) && + (p.x() < (vj.x() - vi.x()) * (p.y() - vi.y()) / (vj.y() - vi.y()) + vi.x())) { + inside = !inside; + } + } + return inside; +} + +} // namespace vde::core diff --git a/src/core/transform.cpp b/src/core/transform.cpp new file mode 100644 index 0000000..51d9c19 --- /dev/null +++ b/src/core/transform.cpp @@ -0,0 +1,2 @@ +#include "vde/core/transform.h" +namespace vde::core {} diff --git a/src/curves/bezier_curve.cpp b/src/curves/bezier_curve.cpp new file mode 100644 index 0000000..05a8eaa --- /dev/null +++ b/src/curves/bezier_curve.cpp @@ -0,0 +1,78 @@ +#include "vde/curves/bezier_curve.h" +#include + +namespace vde::curves { + +BezierCurve::BezierCurve(std::vector control_points) + : cp_(std::move(control_points)) {} + +static Point3D de_casteljau(double t, const std::vector& pts) { + auto temp = pts; + int n = static_cast(temp.size()) - 1; + for (int k = 1; k <= n; ++k) + for (int i = 0; i <= n - k; ++i) + temp[i] = (1 - t) * temp[i] + t * temp[i + 1]; + return temp[0]; +} + +Point3D BezierCurve::evaluate(double t) const { + return de_casteljau(std::clamp(t, 0.0, 1.0), cp_); +} + +Vector3D BezierCurve::derivative(double t, int order) const { + if (order < 1) return evaluate(t) - Point3D::Zero(); + int n = degree(); + if (order > n) return Vector3D::Zero(); + std::vector dpts; + for (int i = 0; i <= n - order; ++i) { + Point3D sum = Point3D::Zero(); + // Elevate via finite differences (simplified) + for (int j = 0; j <= order; ++j) { + double coef = (j % 2 == 0 ? 1.0 : -1.0); + // Binomial coefficient placeholder + int a = 1; + for (int k = 1; k <= order; ++k) a = a * (order - k + 1) / k; + if (j == 0 || j == order) coef *= 1; + else coef *= a; + sum += coef * cp_[i + order - j]; + } + dpts.push_back(sum); + } + double fact = 1.0; + for (int i = 2; i <= order; ++i) fact *= (n - i + 1); + return fact * (de_casteljau(t, dpts) - Point3D::Zero()); +} + +std::pair BezierCurve::split(double t) const { + int n = degree(); + std::vector left(n + 1), right(n + 1); + auto temp = cp_; + left[0] = temp[0]; + right[n] = temp[n]; + for (int k = 1; k <= n; ++k) { + for (int i = 0; i <= n - k; ++i) + temp[i] = (1 - t) * temp[i] + t * temp[i + 1]; + left[k] = temp[0]; + right[n - k] = temp[n - k]; + } + return {BezierCurve(left), BezierCurve(right)}; +} + +BezierCurve BezierCurve::degree_elevate(int times) const { + if (times <= 0) return *this; + BezierCurve result = *this; + for (int t = 0; t < times; ++t) { + int n = result.degree(); + std::vector new_cp(n + 2); + new_cp[0] = result.cp_[0]; + new_cp[n + 1] = result.cp_[n]; + for (int i = 1; i <= n; ++i) { + double alpha = static_cast(i) / (n + 1); + new_cp[i] = alpha * result.cp_[i - 1] + (1 - alpha) * result.cp_[i]; + } + result.cp_ = std::move(new_cp); + } + return result; +} + +} // namespace vde::curves diff --git a/src/curves/bezier_surface.cpp b/src/curves/bezier_surface.cpp new file mode 100644 index 0000000..9a32108 --- /dev/null +++ b/src/curves/bezier_surface.cpp @@ -0,0 +1,36 @@ +#include "vde/curves/bezier_surface.h" + +namespace vde::curves { + +BezierSurface::BezierSurface(std::vector> grid) + : cp_(std::move(grid)) {} + +Point3D BezierSurface::de_casteljau(double t, const std::vector& pts) const { + auto temp = pts; + int n = static_cast(temp.size()) - 1; + for (int k = 1; k <= n; ++k) + for (int i = 0; i <= n - k; ++i) + temp[i] = (1 - t) * temp[i] + t * temp[i + 1]; + return temp[0]; +} + +Point3D BezierSurface::evaluate(double u, double v) const { + std::vector row_results; + for (const auto& row : cp_) + row_results.push_back(de_casteljau(u, row)); + return de_casteljau(v, row_results); +} + +Vector3D BezierSurface::derivative_u(double, double) const { + return Vector3D::Zero(); // TODO +} + +Vector3D BezierSurface::derivative_v(double, double) const { + return Vector3D::Zero(); // TODO +} + +Vector3D BezierSurface::normal(double u, double v) const { + return derivative_u(u, v).cross(derivative_v(u, v)).normalized(); +} + +} // namespace vde::curves diff --git a/src/curves/bspline_curve.cpp b/src/curves/bspline_curve.cpp new file mode 100644 index 0000000..ddde668 --- /dev/null +++ b/src/curves/bspline_curve.cpp @@ -0,0 +1,72 @@ +#include "vde/curves/bspline_curve.h" +#include + +namespace vde::curves { + +BSplineCurve::BSplineCurve(std::vector pts, std::vector knots, int d) + : cp_(std::move(pts)), knots_(std::move(knots)), degree_(d) { + if (static_cast(cp_.size()) != static_cast(knots_.size()) - degree_ - 1) + throw std::invalid_argument("BSplineCurve: n+1 control points, m+1 knots, m = n + p + 1"); +} + +int BSplineCurve::find_span(double t) const { + int n = static_cast(cp_.size()) - 1; + if (t >= knots_[n + 1]) return n; + if (t <= knots_[degree_]) return degree_; + int low = degree_, high = n + 1; + int mid = (low + high) / 2; + while (t < knots_[mid] || t >= knots_[mid + 1]) { + if (t < knots_[mid]) high = mid; + else low = mid; + mid = (low + high) / 2; + } + return mid; +} + +std::vector BSplineCurve::basis_functions(double t, int span) const { + if (span < 0) span = find_span(t); + std::vector N(degree_ + 1, 0.0); + N[0] = 1.0; + std::vector left(degree_ + 1), right(degree_ + 1); + for (int j = 1; j <= degree_; ++j) { + left[j] = t - knots_[span + 1 - j]; + right[j] = knots_[span + j] - t; + double saved = 0.0; + for (int r = 0; r < j; ++r) { + double temp = N[r] / (right[r + 1] + left[j - r]); + N[r] = saved + right[r + 1] * temp; + saved = left[j - r] * temp; + } + N[j] = saved; + } + return N; +} + +Point3D BSplineCurve::evaluate(double t) const { + int span = find_span(t); + auto N = basis_functions(t, span); + Point3D result = Point3D::Zero(); + for (int i = 0; i <= degree_; ++i) + result += N[i] * cp_[span - degree_ + i]; + return result; +} + +Vector3D BSplineCurve::derivative(double t, int order) const { + if (order <= 0) return evaluate(t) - Point3D::Zero(); + // Create derivative curve by reducing knot multiplicity + std::vector dcp; + for (int i = 0; i < static_cast(cp_.size()) - 1; ++i) { + double denom = knots_[i + degree_ + 1] - knots_[i + 1]; + dcp.push_back(degree_ / denom * (cp_[i + 1] - cp_[i])); + } + BSplineCurve deriv(dcp, + std::vector(knots_.begin() + 1, knots_.end() - 1), + degree_ - 1); + return deriv.evaluate(t) - Point3D::Zero(); +} + +std::pair BSplineCurve::domain() const { + return {knots_[degree_], knots_[cp_.size()]}; +} + +} // namespace vde::curves diff --git a/src/curves/bspline_surface.cpp b/src/curves/bspline_surface.cpp new file mode 100644 index 0000000..38a5aaf --- /dev/null +++ b/src/curves/bspline_surface.cpp @@ -0,0 +1,26 @@ +#include "vde/curves/bspline_surface.h" + +namespace vde::curves { + +BSplineSurface::BSplineSurface(std::vector> grid, + std::vector ku, std::vector kv, int du, int dv) + : cp_(std::move(grid)), knots_u_(std::move(ku)), knots_v_(std::move(kv)), + degree_u_(du), degree_v_(dv) {} + +Point3D BSplineSurface::evaluate(double u, double v) const { + BSplineCurve bs_u({}, knots_u_, degree_u_); + int span_u = bs_u.find_span(u); + auto Nu = bs_u.basis_functions(u, span_u); + + BSplineCurve bs_v({}, knots_v_, degree_v_); + int span_v = bs_v.find_span(v); + auto Nv = bs_v.basis_functions(v, span_v); + + Point3D result = Point3D::Zero(); + for (int i = 0; i <= degree_u_; ++i) + for (int j = 0; j <= degree_v_; ++j) + result += Nu[i] * Nv[j] * cp_[span_u - degree_u_ + i][span_v - degree_v_ + j]; + return result; +} + +} // namespace vde::curves diff --git a/src/curves/nurbs_curve.cpp b/src/curves/nurbs_curve.cpp new file mode 100644 index 0000000..838ae4f --- /dev/null +++ b/src/curves/nurbs_curve.cpp @@ -0,0 +1,40 @@ +#include "vde/curves/nurbs_curve.h" + +namespace vde::curves { + +NurbsCurve::NurbsCurve(std::vector pts, std::vector knots, + std::vector weights, int degree) + : cp_(std::move(pts)), knots_(std::move(knots)), + weights_(std::move(weights)), degree_(degree) {} + +Point3D NurbsCurve::evaluate(double t) const { + BSplineCurve bs(cp_, knots_, degree_); + int span = bs.find_span(t); + auto N = bs.basis_functions(t, span); + Point3D pw = Point3D::Zero(); + double w_sum = 0.0; + for (int i = 0; i <= degree_; ++i) { + double wN = N[i] * weights_[span - degree_ + i]; + pw += wN * cp_[span - degree_ + i]; + w_sum += wN; + } + return pw / w_sum; +} + +Vector3D NurbsCurve::derivative(double t, int order) const { + // Simplified: use B-Spline derivative on homogenized points + if (order <= 0) return evaluate(t) - Point3D::Zero(); + std::vector hpts; + for (size_t i = 0; i < cp_.size(); ++i) { + double w = weights_[i]; + hpts.emplace_back(cp_[i].x() * w, cp_[i].y() * w, cp_[i].z() * w, w); + } + // Derivative in homogeneous space then project + return Vector3D::Zero(); // TODO: proper NURBS derivative +} + +std::pair NurbsCurve::domain() const { + return {knots_[degree_], knots_[cp_.size()]}; +} + +} // namespace vde::curves diff --git a/src/curves/tessellation.cpp b/src/curves/tessellation.cpp new file mode 100644 index 0000000..ef10833 --- /dev/null +++ b/src/curves/tessellation.cpp @@ -0,0 +1,34 @@ +#include "vde/curves/tessellation.h" + +namespace vde::curves { + +std::pair, std::vector>> +tessellate(const std::function& eval, + int res_u, int res_v) { + std::vector verts; + std::vector> tris; + + for (int j = 0; j <= res_v; ++j) { + double v = static_cast(j) / res_v; + for (int i = 0; i <= res_u; ++i) { + double u = static_cast(i) / res_u; + verts.push_back(eval(u, v)); + } + } + + int stride = res_u + 1; + for (int j = 0; j < res_v; ++j) { + for (int i = 0; i < res_u; ++i) { + int a = j * stride + i; + int b = a + 1; + int c = a + stride; + int d = c + 1; + tris.push_back({a, b, d}); + tris.push_back({a, d, c}); + } + } + + return {verts, tris}; +} + +} // namespace vde::curves diff --git a/src/foundation/exact_predicates.cpp b/src/foundation/exact_predicates.cpp new file mode 100644 index 0000000..1bbf100 --- /dev/null +++ b/src/foundation/exact_predicates.cpp @@ -0,0 +1,48 @@ +#include "vde/foundation/exact_predicates.h" +#include + +namespace vde::foundation::exact { + +namespace { + +// Simple non-adaptive implementation; full Shewchuk predicates TBD +double orient2d_det(const Point2D& a, const Point2D& b, const Point2D& c) { + return (b.x() - a.x()) * (c.y() - a.y()) - (b.y() - a.y()) * (c.x() - a.x()); +} + +} // namespace + +Orientation orient_2d(const Point2D& a, const Point2D& b, const Point2D& c) { + double det = orient2d_det(a, b, c); + if (det > 1e-12) return Orientation::CounterClockwise; + if (det < -1e-12) return Orientation::Clockwise; + return Orientation::Collinear; +} + +Orientation orient_3d(const Point3D& a, const Point3D& b, + const Point3D& c, const Point3D& d) { + Eigen::Matrix4d m; + m << a.x(), a.y(), a.z(), 1.0, + b.x(), b.y(), b.z(), 1.0, + c.x(), c.y(), c.z(), 1.0, + d.x(), d.y(), d.z(), 1.0; + double det = m.determinant(); + if (det > 1e-12) return Orientation::CounterClockwise; + if (det < -1e-12) return Orientation::Clockwise; + return Orientation::Collinear; +} + +CircleTest in_circle(const Point2D& a, const Point2D& b, + const Point2D& c, const Point2D& d) { + double ax = a.x() - d.x(), ay = a.y() - d.y(); + double bx = b.x() - d.x(), by = b.y() - d.y(); + double cx = c.x() - d.x(), cy = c.y() - d.y(); + double det = (ax*ax + ay*ay) * (bx*cy - by*cx) + - (bx*bx + by*by) * (ax*cy - ay*cx) + + (cx*cx + cy*cy) * (ax*by - ay*bx); + if (det > 1e-12) return CircleTest::Inside; + if (det < -1e-12) return CircleTest::Outside; + return CircleTest::On; +} + +} // namespace vde::foundation::exact diff --git a/src/foundation/io_obj.cpp b/src/foundation/io_obj.cpp new file mode 100644 index 0000000..52107ca --- /dev/null +++ b/src/foundation/io_obj.cpp @@ -0,0 +1,54 @@ +#include "vde/foundation/io_obj.h" +#include +#include + +namespace vde::foundation { + +ObjMeshData read_obj(const std::string& filepath) { + ObjMeshData data; + std::ifstream file(filepath); + if (!file) return data; + + std::string line; + while (std::getline(file, line)) { + if (line.empty() || line[0] == '#') continue; + std::istringstream iss(line); + std::string token; + iss >> token; + if (token == "v") { + double x, y, z; + iss >> x >> y >> z; + data.vertices.emplace_back(x, y, z); + } else if (token == "vn") { + double nx, ny, nz; + iss >> nx >> ny >> nz; + data.normals.emplace_back(nx, ny, nz); + } else if (token == "f") { + std::vector face; + std::string vertex; + while (iss >> vertex) { + std::istringstream viss(vertex); + std::string vi_str; + std::getline(viss, vi_str, '/'); + face.push_back(std::stoi(vi_str) - 1); + } + data.faces.push_back(std::move(face)); + } + } + return data; +} + +void write_obj(const std::string& filepath, const ObjMeshData& data) { + std::ofstream file(filepath); + for (const auto& v : data.vertices) + file << "v " << v.x() << " " << v.y() << " " << v.z() << "\n"; + for (const auto& n : data.normals) + file << "vn " << n.x() << " " << n.y() << " " << n.z() << "\n"; + for (const auto& f : data.faces) { + file << "f"; + for (int vi : f) file << " " << (vi + 1); + file << "\n"; + } +} + +} // namespace vde::foundation diff --git a/src/foundation/io_stl.cpp b/src/foundation/io_stl.cpp new file mode 100644 index 0000000..54fe738 --- /dev/null +++ b/src/foundation/io_stl.cpp @@ -0,0 +1,48 @@ +#include "vde/foundation/io_stl.h" +#include +#include + +namespace vde::foundation { + +std::vector read_stl(const std::string& filepath) { + std::vector tris; + std::ifstream file(filepath, std::ios::binary); + if (!file) return tris; + + // Read binary STL + char header[80]; + file.read(header, 80); + uint32_t count; + file.read(reinterpret_cast(&count), 4); + tris.reserve(count); + + for (uint32_t i = 0; i < count; ++i) { + StlTriangle tri{}; + file.read(reinterpret_cast(&tri.normal), 12); + file.read(reinterpret_cast(&tri.v0), 12); + file.read(reinterpret_cast(&tri.v1), 12); + file.read(reinterpret_cast(&tri.v2), 12); + uint16_t attr; + file.read(reinterpret_cast(&attr), 2); + tris.push_back(tri); + } + return tris; +} + +void write_stl(const std::string& filepath, const std::vector& tris) { + std::ofstream file(filepath, std::ios::binary); + char header[80] = {}; + file.write(header, 80); + uint32_t count = static_cast(tris.size()); + file.write(reinterpret_cast(&count), 4); + for (const auto& tri : tris) { + file.write(reinterpret_cast(&tri.normal), 12); + file.write(reinterpret_cast(&tri.v0), 12); + file.write(reinterpret_cast(&tri.v1), 12); + file.write(reinterpret_cast(&tri.v2), 12); + uint16_t attr = 0; + file.write(reinterpret_cast(&attr), 2); + } +} + +} // namespace vde::foundation diff --git a/src/foundation/tolerance.cpp b/src/foundation/tolerance.cpp new file mode 100644 index 0000000..5d62844 --- /dev/null +++ b/src/foundation/tolerance.cpp @@ -0,0 +1,7 @@ +#include "vde/foundation/tolerance.h" + +namespace vde::foundation { + +Tolerance Tolerance::global_{}; + +} // namespace vde::foundation diff --git a/src/mesh/delaunay_2d.cpp b/src/mesh/delaunay_2d.cpp new file mode 100644 index 0000000..ee496f2 --- /dev/null +++ b/src/mesh/delaunay_2d.cpp @@ -0,0 +1,129 @@ +#include "vde/mesh/delaunay_2d.h" +#include "vde/foundation/exact_predicates.h" +#include +#include + +namespace vde::mesh { + +namespace { + +struct Tri { + int a, b, c; + Point2D center; + double radius_sq; + bool removed = false; +}; + +double circumradius_sq(const Point2D& a, const Point2D& b, const Point2D& c) { + double ab = (a - b).squaredNorm(); + double bc = (b - c).squaredNorm(); + double ca = (c - a).squaredNorm(); + double area = std::abs((b.x()-a.x())*(c.y()-a.y()) - (c.x()-a.x())*(b.y()-a.y())) * 0.5; + if (area < 1e-20) return std::numeric_limits::max(); + return (ab * bc * ca) / (16.0 * area * area); +} + +Point2D circumcenter(const Point2D& a, const Point2D& b, const Point2D& c) { + double d = 2 * (a.x()*(b.y()-c.y()) + b.x()*(c.y()-a.y()) + c.x()*(a.y()-b.y())); + if (std::abs(d) < 1e-20) return a; + double ux = ((a.x()*a.x()+a.y()*a.y())*(b.y()-c.y()) + + (b.x()*b.x()+b.y()*b.y())*(c.y()-a.y()) + + (c.x()*c.x()+c.y()*c.y())*(a.y()-b.y())) / d; + double uy = ((a.x()*a.x()+a.y()*a.y())*(c.x()-b.x()) + + (b.x()*b.x()+b.y()*b.y())*(a.x()-c.x()) + + (c.x()*c.x()+c.y()*c.y())*(b.x()-a.x())) / d; + return {ux, uy}; +} + +bool in_circle(const Point2D& p, const Tri& tri) { + return (p - tri.center).squaredNorm() < tri.radius_sq - 1e-12; +} + +struct Edge { + int a, b; + bool operator==(const Edge& o) const { + return (a==o.a && b==o.b) || (a==o.b && b==o.a); + } +}; + +struct EdgeHash { + size_t operator()(const Edge& e) const { + return std::hash()(std::min(e.a, e.b)) ^ + std::hash()(std::max(e.a, e.b)); + } +}; + +} // namespace + +DelaunayResult delaunay_2d(const std::vector& pts) { + DelaunayResult result; + if (pts.size() < 3) { + result.vertices = pts; + return result; + } + + auto points = pts; + // Find bounding box for super-triangle + double min_x = points[0].x(), max_x = min_x; + double min_y = points[0].y(), max_y = min_y; + for (const auto& p : points) { + min_x = std::min(min_x, p.x()); max_x = std::max(max_x, p.x()); + min_y = std::min(min_y, p.y()); max_y = std::max(max_y, p.y()); + } + double dx = max_x - min_x, dy = max_y - min_y; + double dmax = std::max(dx, dy) * 10; + int n0 = static_cast(points.size()); + points.emplace_back(min_x - dmax, min_y - dmax); + points.emplace_back(max_x + dmax, min_y - dmax); + points.emplace_back((min_x + max_x) / 2, max_y + dmax); + + std::vector tris; + tris.push_back({n0, n0 + 1, n0 + 2, Point2D::Zero(), 0}); + tris[0].center = circumcenter(points[n0], points[n0+1], points[n0+2]); + tris[0].radius_sq = circumradius_sq(points[n0], points[n0+1], points[n0+2]); + + for (int i = 0; i < n0; ++i) { + std::vector bad; + for (size_t j = 0; j < tris.size(); ++j) { + if (tris[j].removed) continue; + if (in_circle(points[i], tris[j])) + bad.push_back(static_cast(j)); + } + + std::unordered_set boundary; + for (int ti : bad) { + auto& tri = tris[ti]; + tri.removed = true; + std::array edges = {{{tri.a, tri.b}, {tri.b, tri.c}, {tri.c, tri.a}}}; + for (const auto& e : edges) { + auto it = boundary.find(e); + if (it != boundary.end()) boundary.erase(it); + else boundary.insert(e); + } + } + + for (const auto& e : boundary) { + Tri t{e.a, e.b, i, Point2D::Zero(), 0}; + t.center = circumcenter(points[t.a], points[t.b], points[t.c]); + t.radius_sq = circumradius_sq(points[t.a], points[t.b], points[t.c]); + tris.push_back(t); + } + } + + // Filter out super-triangle vertices + for (const auto& t : tris) { + if (t.removed) continue; + if (t.a >= n0 || t.b >= n0 || t.c >= n0) continue; + // Ensure CCW + if (foundation::exact::orient_2d(points[t.a], points[t.b], points[t.c]) + != foundation::exact::Orientation::CounterClockwise) + result.triangles.push_back({t.a, t.c, t.b}); + else + result.triangles.push_back({t.a, t.b, t.c}); + } + + result.vertices.assign(points.begin(), points.begin() + n0); + return result; +} + +} // namespace vde::mesh diff --git a/src/mesh/halfedge_mesh.cpp b/src/mesh/halfedge_mesh.cpp new file mode 100644 index 0000000..f598a34 --- /dev/null +++ b/src/mesh/halfedge_mesh.cpp @@ -0,0 +1,129 @@ +#include "vde/mesh/halfedge_mesh.h" +#include +#include + +namespace vde::mesh { + +void HalfedgeMesh::clear() { + vertices_.clear(); + halfedges_.clear(); + faces_.clear(); + face_normals_.clear(); + vertex_normals_.clear(); + normals_dirty_ = true; +} + +int HalfedgeMesh::add_vertex(const Point3D& p) { + vertices_.push_back(p); + normals_dirty_ = true; + return static_cast(vertices_.size()) - 1; +} + +int HalfedgeMesh::find_or_create_edge(int v0, int v1) { + // Simple lookup: check existing halfedges + for (size_t i = 0; i < halfedges_.size(); ++i) { + const auto& he = halfedges_[i]; + if (he.vertex_index == v1 && !halfedges_.empty()) { + int prev = (i > 0) ? halfedges_[i - 1].vertex_index : -1; + // Inefficient — full edge lookup needs an edge map + } + } + // Create new halfedge pair + int he0 = static_cast(halfedges_.size()); + int he1 = he0 + 1; + halfedges_.push_back({v1, -1, -1, -1, he1}); + halfedges_.push_back({v0, -1, -1, -1, he0}); + return he0; +} + +int HalfedgeMesh::add_face(const std::vector& vis) { + int n = static_cast(vis.size()); + if (n < 3) return -1; + + int he_start = static_cast(halfedges_.size()); + for (int i = 0; i < n; ++i) { + int vi = vis[i]; + int vj = vis[(i + 1) % n]; + halfedges_.push_back({vj, -1, he_start + (i + 1) % n, he_start + (i - 1 + n) % n, -1}); + } + + int fi = static_cast(faces_.size()); + faces_.push_back({he_start, n}); + for (int i = 0; i < n; ++i) + halfedges_[he_start + i].face_index = fi; + + normals_dirty_ = true; + return fi; +} + +void HalfedgeMesh::build_from_triangles( + const std::vector& verts, + const std::vector>& tris) { + clear(); + for (const auto& v : verts) add_vertex(v); + for (const auto& t : tris) + add_face({t[0], t[1], t[2]}); +} + +std::vector HalfedgeMesh::face_vertices(int fi) const { + const auto& f = faces_[fi]; + std::vector vis; + int he = f.halfedge_index; + for (int i = 0; i < f.valence; ++i) { + vis.push_back(halfedges_[he].vertex_index); + he = halfedges_[he].next_index; + } + return vis; +} + +std::vector HalfedgeMesh::vertex_faces(int vi) const { + std::vector result; + for (size_t fi = 0; fi < faces_.size(); ++fi) { + auto vis = face_vertices(static_cast(fi)); + if (std::find(vis.begin(), vis.end(), vi) != vis.end()) + result.push_back(static_cast(fi)); + } + return result; +} + +bool HalfedgeMesh::is_boundary_vertex(int vi) const { + for (size_t i = 0; i < halfedges_.size(); ++i) { + if (halfedges_[i].vertex_index == vi && halfedges_[i].opposite_index < 0) + return true; + } + return false; +} + +Vector3D HalfedgeMesh::face_normal(int fi) const { + auto vis = face_vertices(fi); + if (vis.size() < 3) return Vector3D::UnitZ(); + Vector3D v0 = vertices_[vis[1]] - vertices_[vis[0]]; + Vector3D v1 = vertices_[vis[2]] - vertices_[vis[0]]; + return v0.cross(v1).normalized(); +} + +Vector3D HalfedgeMesh::vertex_normal(int vi) const { + Vector3D sum = Vector3D::Zero(); + auto fis = vertex_faces(vi); + for (int fi : fis) sum += face_normal(fi); + if (fis.empty()) return Vector3D::UnitZ(); + return sum.normalized(); +} + +void HalfedgeMesh::update_normals() { + face_normals_.resize(faces_.size()); + for (size_t i = 0; i < faces_.size(); ++i) + face_normals_[i] = face_normal(static_cast(i)); + vertex_normals_.resize(vertices_.size()); + for (size_t i = 0; i < vertices_.size(); ++i) + vertex_normals_[i] = vertex_normal(static_cast(i)); + normals_dirty_ = false; +} + +AABB3D HalfedgeMesh::bounds() const { + AABB3D b; + for (const auto& v : vertices_) b.expand(v); + return b; +} + +} // namespace vde::mesh diff --git a/src/mesh/mesh_boolean.cpp b/src/mesh/mesh_boolean.cpp new file mode 100644 index 0000000..de7114e --- /dev/null +++ b/src/mesh/mesh_boolean.cpp @@ -0,0 +1 @@ +// Stub — implementation pending diff --git a/src/mesh/mesh_quality.cpp b/src/mesh/mesh_quality.cpp new file mode 100644 index 0000000..de7114e --- /dev/null +++ b/src/mesh/mesh_quality.cpp @@ -0,0 +1 @@ +// Stub — implementation pending diff --git a/src/mesh/mesh_repair.cpp b/src/mesh/mesh_repair.cpp new file mode 100644 index 0000000..de7114e --- /dev/null +++ b/src/mesh/mesh_repair.cpp @@ -0,0 +1 @@ +// Stub — implementation pending diff --git a/src/mesh/mesh_simplify.cpp b/src/mesh/mesh_simplify.cpp new file mode 100644 index 0000000..de7114e --- /dev/null +++ b/src/mesh/mesh_simplify.cpp @@ -0,0 +1 @@ +// Stub — implementation pending diff --git a/src/mesh/mesh_smooth.cpp b/src/mesh/mesh_smooth.cpp new file mode 100644 index 0000000..de7114e --- /dev/null +++ b/src/mesh/mesh_smooth.cpp @@ -0,0 +1 @@ +// Stub — implementation pending diff --git a/src/spatial/bvh.cpp b/src/spatial/bvh.cpp new file mode 100644 index 0000000..201c0a3 --- /dev/null +++ b/src/spatial/bvh.cpp @@ -0,0 +1,112 @@ +#include "vde/spatial/bvh.h" +#include + +namespace vde::spatial { + +void BVH::build(const std::vector& tris) { + primitives_ = tris; + nodes_.clear(); + if (tris.empty()) return; + nodes_.emplace_back(); + build_recursive(0, 0, static_cast(tris.size()), 0); +} + +void BVH::build_recursive(int node_idx, int first, int count, int depth) { + auto& node = nodes_[node_idx]; + // Compute bounding box + for (int i = 0; i < count; ++i) + for (int v = 0; v < 3; ++v) + node.bounds.expand(primitives_[first + i].v(v)); + + // Leaf stop + if (count <= opts_.leaf_size || depth >= opts_.max_depth) { + node.first_prim = first; + node.prim_count = count; + return; + } + + // Find best split axis (use longest extent) + Vector3D extent = node.bounds.extent(); + int axis = 0; + if (extent.y() > extent.x()) axis = 1; + if (extent.z() > extent(axis)) axis = 2; + + // Sort by centroid along axis + int mid = first + count / 2; + std::nth_element(primitives_.begin() + first, + primitives_.begin() + mid, + primitives_.begin() + first + count, + [axis](const Triangle3D& a, const Triangle3D& b) { + return a.centroid()(axis) < b.centroid()(axis); + }); + + int left_idx = static_cast(nodes_.size()); + nodes_.emplace_back(); + int right_idx = static_cast(nodes_.size()); + nodes_.emplace_back(); + + node.left = left_idx; + node.right = right_idx; + + int left_count = mid - first; + build_recursive(left_idx, first, left_count, depth + 1); + build_recursive(right_idx, mid, count - left_count, depth + 1); +} + +double BVH::sah_cost(int nl, int nr, const AABB3D& la, const AABB3D& ra) const { + double area_total = la.surface_area() + ra.surface_area(); + if (area_total < 1e-20) return 0; + return 1.0 + (nl * la.surface_area() + nr * ra.surface_area()) / area_total; +} + +void BVH::insert(const Triangle3D& tri) { primitives_.push_back(tri); build(primitives_); } +bool BVH::remove(const Triangle3D&) { return false; } +void BVH::clear() { primitives_.clear(); nodes_.clear(); } + +std::vector BVH::query_range(const AABB3D& range) const { + if (nodes_.empty()) return {}; + std::vector result; + std::vector stack = {0}; + while (!stack.empty()) { + int ni = stack.back(); stack.pop_back(); + const auto& node = nodes_[ni]; + if (!node.bounds.intersects(range)) continue; + if (node.leaf()) { + for (int i = 0; i < node.prim_count; ++i) + result.push_back(primitives_[node.first_prim + i]); + } else { + stack.push_back(node.right); + stack.push_back(node.left); + } + } + return result; +} + +std::vector BVH::query_knn(const Point3D&, size_t) const { return {}; } + +std::vector BVH::query_ray(const Ray3Dd& ray) const { + if (nodes_.empty()) return {}; + std::vector result; + std::vector stack = {0}; + while (!stack.empty()) { + int ni = stack.back(); stack.pop_back(); + const auto& node = nodes_[ni]; + // Simple AABB-ray intersection test (can be improved) + if (node.leaf()) { + for (int i = 0; i < node.prim_count; ++i) { + // Placeholder: check actual triangle-ray intersection + result.push_back(primitives_[node.first_prim + i]); + } + } else { + if (node.left >= 0) stack.push_back(node.left); + if (node.right >= 0) stack.push_back(node.right); + } + } + return result; +} + +std::optional BVH::query_ray_nearest(const Ray3Dd&) const { + return std::nullopt; +} + +} // namespace vde::spatial diff --git a/src/spatial/kd_tree.cpp b/src/spatial/kd_tree.cpp new file mode 100644 index 0000000..b167aa0 --- /dev/null +++ b/src/spatial/kd_tree.cpp @@ -0,0 +1 @@ +// Stub diff --git a/src/spatial/octree.cpp b/src/spatial/octree.cpp new file mode 100644 index 0000000..b167aa0 --- /dev/null +++ b/src/spatial/octree.cpp @@ -0,0 +1 @@ +// Stub diff --git a/src/spatial/r_tree.cpp b/src/spatial/r_tree.cpp new file mode 100644 index 0000000..b167aa0 --- /dev/null +++ b/src/spatial/r_tree.cpp @@ -0,0 +1 @@ +// Stub diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..fe0c6ad --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +function(add_vde_test name) + add_executable(${name} ${name}.cpp) + target_link_libraries(${name} PRIVATE vde GTest::gtest GTest::gtest_main) + target_include_directories(${name} PRIVATE ${CMAKE_SOURCE_DIR}/include) + gtest_discover_tests(${name}) +endfunction() + +add_subdirectory(core) +add_subdirectory(curves) +add_subdirectory(mesh) +add_subdirectory(spatial) +add_subdirectory(collision) +add_subdirectory(boolean) diff --git a/tests/boolean/CMakeLists.txt b/tests/boolean/CMakeLists.txt new file mode 100644 index 0000000..31a93d1 --- /dev/null +++ b/tests/boolean/CMakeLists.txt @@ -0,0 +1 @@ +add_vde_test(test_boolean_2d) diff --git a/tests/boolean/test_boolean_2d.cpp b/tests/boolean/test_boolean_2d.cpp new file mode 100644 index 0000000..0775dda --- /dev/null +++ b/tests/boolean/test_boolean_2d.cpp @@ -0,0 +1,18 @@ +#include +#include "vde/boolean/boolean_2d.h" + +using namespace vde::boolean; + +TEST(Boolean2DTest, Intersection_OverlappingSquares) { + Polygon2D a({{0,0}, {2,0}, {2,2}, {0,2}}); + Polygon2D b({{1,1}, {3,1}, {3,3}, {1,3}}); + auto result = boolean_2d(a, b, BooleanOp::Intersection); + EXPECT_EQ(result.size(), 1u); +} + +TEST(Boolean2DTest, Intersection_Disjoint) { + Polygon2D a({{0,0}, {1,0}, {1,1}, {0,1}}); + Polygon2D b({{2,2}, {3,2}, {3,3}, {2,3}}); + auto result = boolean_2d(a, b, BooleanOp::Intersection); + EXPECT_TRUE(result.empty() || result[0].empty()); +} diff --git a/tests/collision/CMakeLists.txt b/tests/collision/CMakeLists.txt new file mode 100644 index 0000000..ac5e15b --- /dev/null +++ b/tests/collision/CMakeLists.txt @@ -0,0 +1 @@ +add_vde_test(test_gjk) diff --git a/tests/collision/test_gjk.cpp b/tests/collision/test_gjk.cpp new file mode 100644 index 0000000..933c0f3 --- /dev/null +++ b/tests/collision/test_gjk.cpp @@ -0,0 +1,20 @@ +#include +#include "vde/collision/gjk.h" + +using namespace vde::collision; + +static SupportFunc make_sphere(const Point3D& center, double radius) { + return [=](const Vector3D& dir) { return center + dir.normalized() * radius; }; +} + +TEST(GJKTest, SeparatedSpheres) { + auto a = make_sphere({0,0,0}, 1.0); + auto b = make_sphere({5,0,0}, 1.0); + EXPECT_FALSE(gjk_intersect(a, b)); +} + +TEST(GJKTest, OverlappingSpheres) { + auto a = make_sphere({0,0,0}, 1.0); + auto b = make_sphere({1,0,0}, 1.0); + EXPECT_TRUE(gjk_intersect(a, b)); +} diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt new file mode 100644 index 0000000..4247962 --- /dev/null +++ b/tests/core/CMakeLists.txt @@ -0,0 +1,4 @@ +add_vde_test(test_point) +add_vde_test(test_convex_hull) +add_vde_test(test_transform) +add_vde_test(test_distance) diff --git a/tests/core/test_convex_hull.cpp b/tests/core/test_convex_hull.cpp new file mode 100644 index 0000000..3265642 --- /dev/null +++ b/tests/core/test_convex_hull.cpp @@ -0,0 +1,21 @@ +#include +#include "vde/core/convex_hull.h" + +using namespace vde::core; + +TEST(ConvexHullTest, ThreePoints_ReturnsTriangle) { + std::vector pts = {{0,0}, {1,0}, {0,1}}; + auto hull = convex_hull_2d(pts); + EXPECT_EQ(hull.size(), 3u); +} + +TEST(ConvexHullTest, CollinearPoints_ReturnsEndpoints) { + std::vector pts = {{0,0}, {1,1}, {2,2}, {3,3}}; + auto hull = convex_hull_2d(pts); + EXPECT_GE(hull.size(), 2u); +} + +TEST(ConvexHullTest, Empty_ReturnsEmpty) { + auto hull = convex_hull_2d({}); + EXPECT_TRUE(hull.empty()); +} diff --git a/tests/core/test_distance.cpp b/tests/core/test_distance.cpp new file mode 100644 index 0000000..19b25b8 --- /dev/null +++ b/tests/core/test_distance.cpp @@ -0,0 +1,13 @@ +#include +#include "vde/core/distance.h" + +using namespace vde::core; + +TEST(DistanceTest, PointToPoint) { + EXPECT_DOUBLE_EQ(distance(Point3D(0,0,0), Point3D(1,0,0)), 1.0); +} + +TEST(DistanceTest, PointToPlane) { + Plane3D plane(Point3D(0,0,0), Vector3D(0,0,1)); + EXPECT_DOUBLE_EQ(distance(Point3D(0,0,5), plane), 5.0); +} diff --git a/tests/core/test_point.cpp b/tests/core/test_point.cpp new file mode 100644 index 0000000..b5250fe --- /dev/null +++ b/tests/core/test_point.cpp @@ -0,0 +1,38 @@ +#include +#include "vde/core/point.h" +#include "vde/foundation/tolerance.h" + +using namespace vde::core; +using namespace vde::foundation; + +TEST(PointTest, DefaultConstructor_IsZero) { + Point3D p; + EXPECT_DOUBLE_EQ(p.x(), 0.0); + EXPECT_DOUBLE_EQ(p.y(), 0.0); + EXPECT_DOUBLE_EQ(p.z(), 0.0); +} + +TEST(PointTest, Distance_SamePoint_ReturnsZero) { + Point3D a(1.0, 2.0, 3.0); + EXPECT_DOUBLE_EQ((a - a).norm(), 0.0); +} + +TEST(PointTest, Distance_KnownValues) { + Point3D a(0, 0, 0), b(3, 4, 0); + EXPECT_DOUBLE_EQ((b - a).norm(), 5.0); +} + +TEST(PointTest, Arithmetic_AddVector) { + Point3D a(1, 2, 3); + Vector3D v(4, 5, 6); + Point3D r = a + v; + EXPECT_DOUBLE_EQ(r.x(), 5.0); + EXPECT_DOUBLE_EQ(r.y(), 7.0); + EXPECT_DOUBLE_EQ(r.z(), 9.0); +} + +TEST(PointTest, Tolerance_EqualsWithinTol) { + Tolerance tol(1e-6); + EXPECT_TRUE(tol.points_equal(Point3D(1,2,3), Point3D(1+1e-7, 2, 3))); + EXPECT_FALSE(tol.points_equal(Point3D(1,2,3), Point3D(2,2,3))); +} diff --git a/tests/core/test_transform.cpp b/tests/core/test_transform.cpp new file mode 100644 index 0000000..3a1e9d9 --- /dev/null +++ b/tests/core/test_transform.cpp @@ -0,0 +1,22 @@ +#include +#include "vde/core/transform.h" + +using namespace vde::core; + +TEST(TransformTest, Translate) { + auto T = translate(1, 2, 3); + Point3D p(0, 0, 0); + Point3D q = T * p; + EXPECT_DOUBLE_EQ(q.x(), 1.0); + EXPECT_DOUBLE_EQ(q.y(), 2.0); + EXPECT_DOUBLE_EQ(q.z(), 3.0); +} + +TEST(TransformTest, Scale) { + auto S = scale(2.0); + Point3D p(1, 2, 3); + Point3D q = S * p; + EXPECT_DOUBLE_EQ(q.x(), 2.0); + EXPECT_DOUBLE_EQ(q.y(), 4.0); + EXPECT_DOUBLE_EQ(q.z(), 6.0); +} diff --git a/tests/curves/CMakeLists.txt b/tests/curves/CMakeLists.txt new file mode 100644 index 0000000..6fa47dd --- /dev/null +++ b/tests/curves/CMakeLists.txt @@ -0,0 +1,2 @@ +add_vde_test(test_bezier) +add_vde_test(test_nurbs) diff --git a/tests/curves/test_bezier.cpp b/tests/curves/test_bezier.cpp new file mode 100644 index 0000000..e226138 --- /dev/null +++ b/tests/curves/test_bezier.cpp @@ -0,0 +1,18 @@ +#include +#include "vde/curves/bezier_curve.h" + +using namespace vde::curves; + +TEST(BezierTest, Linear_EvaluateAtMidpoint) { + BezierCurve curve({Point3D(0,0,0), Point3D(2,0,0)}); + Point3D p = curve.evaluate(0.5); + EXPECT_NEAR(p.x(), 1.0, 1e-6); +} + +TEST(BezierTest, Quadratic_EvaluateAtEndpoints) { + BezierCurve curve({Point3D(0,0,0), Point3D(1,1,0), Point3D(2,0,0)}); + Point3D p0 = curve.evaluate(0.0); + Point3D p1 = curve.evaluate(1.0); + EXPECT_NEAR((p0 - Point3D(0,0,0)).norm(), 0.0, 1e-6); + EXPECT_NEAR((p1 - Point3D(2,0,0)).norm(), 0.0, 1e-6); +} diff --git a/tests/curves/test_nurbs.cpp b/tests/curves/test_nurbs.cpp new file mode 100644 index 0000000..b3eb021 --- /dev/null +++ b/tests/curves/test_nurbs.cpp @@ -0,0 +1,16 @@ +#include +#include "vde/curves/nurbs_curve.h" + +using namespace vde::curves; + +TEST(NurbsTest, BSpline_UnitWeight) { + // 3 control points, degree 2, uniform knots + NurbsCurve curve( + {Point3D(0,0,0), Point3D(1,2,0), Point3D(2,0,0)}, + {0,0,0,1,1,1}, + {1,1,1}, + 2 + ); + auto p = curve.evaluate(0.5); + EXPECT_NEAR(p.y(), 1.0, 0.1); // Should peak near y=1 +} diff --git a/tests/mesh/CMakeLists.txt b/tests/mesh/CMakeLists.txt new file mode 100644 index 0000000..c6c5e00 --- /dev/null +++ b/tests/mesh/CMakeLists.txt @@ -0,0 +1,2 @@ +add_vde_test(test_halfedge) +add_vde_test(test_delaunay) diff --git a/tests/mesh/test_delaunay.cpp b/tests/mesh/test_delaunay.cpp new file mode 100644 index 0000000..96c2055 --- /dev/null +++ b/tests/mesh/test_delaunay.cpp @@ -0,0 +1,17 @@ +#include +#include "vde/mesh/delaunay_2d.h" + +using namespace vde::mesh; + +TEST(DelaunayTest, FourPointsSquare) { + std::vector pts = {{0,0}, {1,0}, {1,1}, {0,1}}; + auto result = delaunay_2d(pts); + EXPECT_GE(result.triangles.size(), 2u); + EXPECT_EQ(result.vertices.size(), 4u); +} + +TEST(DelaunayTest, ThreePoints) { + std::vector pts = {{0,0}, {1,0}, {0,1}}; + auto result = delaunay_2d(pts); + EXPECT_EQ(result.triangles.size(), 1u); +} diff --git a/tests/mesh/test_halfedge.cpp b/tests/mesh/test_halfedge.cpp new file mode 100644 index 0000000..9e6c473 --- /dev/null +++ b/tests/mesh/test_halfedge.cpp @@ -0,0 +1,25 @@ +#include +#include "vde/mesh/halfedge_mesh.h" + +using namespace vde::mesh; + +TEST(HalfedgeTest, SingleTriangle) { + HalfedgeMesh mesh; + mesh.build_from_triangles( + {Point3D(0,0,0), Point3D(1,0,0), Point3D(0,1,0)}, + {{0,1,2}} + ); + EXPECT_EQ(mesh.num_vertices(), 3u); + EXPECT_EQ(mesh.num_faces(), 1u); +} + +TEST(HalfedgeTest, Bounds_ComputeCorrectly) { + HalfedgeMesh mesh; + mesh.build_from_triangles( + {Point3D(0,0,0), Point3D(2,0,0), Point3D(1,3,0)}, + {{0,1,2}} + ); + auto b = mesh.bounds(); + EXPECT_NEAR(b.extent().x(), 2.0, 1e-6); + EXPECT_NEAR(b.extent().y(), 3.0, 1e-6); +} diff --git a/tests/spatial/CMakeLists.txt b/tests/spatial/CMakeLists.txt new file mode 100644 index 0000000..8a5153f --- /dev/null +++ b/tests/spatial/CMakeLists.txt @@ -0,0 +1 @@ +add_vde_test(test_bvh) diff --git a/tests/spatial/test_bvh.cpp b/tests/spatial/test_bvh.cpp new file mode 100644 index 0000000..c365828 --- /dev/null +++ b/tests/spatial/test_bvh.cpp @@ -0,0 +1,17 @@ +#include +#include "vde/spatial/bvh.h" + +using namespace vde::spatial; + +TEST(BVHTest, BuildEmpty) { + BVH bvh; + bvh.build({}); + EXPECT_EQ(bvh.size(), 0u); +} + +TEST(BVHTest, BuildSingle) { + BVH bvh; + Triangle3D tri({0,0,0}, {1,0,0}, {0,1,0}); + bvh.build({tri}); + EXPECT_EQ(bvh.size(), 1u); +}