Exporting classes containing std:: objects (vector, map, etc) from a dll

本文详细阐述了在DLL中导出基于STL类型的类时可能遇到的问题,以及提供了一种优雅的解决方案,即通过提供STL模板实例的包装类来避免静态成员不一致导致的不可预测行为和随机崩溃。

From: http://www.abstraction.net/ViewArticle.aspx?articleID=83


Related readings:

http://stackoverflow.com/questions/767579/exporting-classes-containing-std-objects-vector-map-etc-from-a-dll

http://www.cplusplus.com/forum/general/78467/


STL and DLLs

 

By George Mihaescu

 

This article is now obsolete: the described behavior applies only up to (and including) Visual Studio 5.0. After that version the STL and compiler implementations have changed so that the behavior described here is not an issue anymore.

 

Summary: this article describes potential problems when exporting from a DLL classes that are based on (or contain public members based on) STL types and provides a solution.

The problem

The use of various classes from STL in DLLs raises a major problem: STL template instantiations cannot be exported directly from the DLL, the main reason being the fact that a few of the STL templates make use of static members. By exposing in a header an STL template instantiation, the DLL and the user of the DLL (that includes the exposed header) get different copies of the static members, and this leads to unpredictable behavior and random crashes.

This problem is documented in the MSDN and various means to resolve it are described.

The solution

The most elegant manner of solving the problem is to provide wrapper classes for the STL template instantiations, and those wrappers are exported from the DLL, as shown below.

For instance, instead of exposing a header from a DLL like this:

 

#include <map>             //for STL map

 

using namespace std;

 

class __declspec(dllexport) MyClass

{

};

 

typedef __declspec (dllexport)  map <int, MyClass>   AMap;

 

 

use a header like this:

 

 

#include <map>             //for STL map

 

using namespace std;

 

class __declspec(dllexport) MyClass

{

};

 

//wrapper class for the map

class __declspec(dllexport) AMap

{

       public:

              AMap ();

              ~AMap ();

 

//wrapper methods for the aggregated map:

              void Insert (const MyClass& m);

              const MyClass* Find (int key);

 

       private:

              typedef map <int, MyClass>   _AMap;

 

       AMap   m_map; //the wrapped map

};

 

 

When implementing the header exposed by the DLL this way, the compiler will generate a warning similar to “class std::map <int, MyClass> must have DLL interface to be used by clients of MyDll.dll”. This is usually with no consequences (it means that friends of the class AMap -if any- will not be able to directly access the wrapped map because it is not exported from the DLL). However, the direct usage of the map by clients of the DLL was not possible anyway due to the problem we were trying to solve in the first place - so this warning is of no concern.


内容概要:本文介绍了ENVI Deep Learning V1.0的操作教程,重点讲解了如何利用ENVI软件进行深度学习模型的训练与应用,以实现遥感图像中特定目标(如集装箱)的自动提取。教程涵盖了从数据准备、标签图像创建、模型初始化与训练,到执行分类及结果优化的完整流程,并介绍了精度评价与通过ENVI Modeler实现一键化建模的方法。系统基于TensorFlow框架,采用ENVINet5(U-Net变体)架构,支持通过点、线、面ROI或分类图生成标签数据,适用于多/高光谱影像的单一类别特征提取。; 适合人群:具备遥感图像处理基础,熟悉ENVI软件操作,从事地理信息、测绘、环境监测等相关领域的技术人员或研究人员,尤其是希望将深度学习技术应用于遥感目标识别的初学者与实践者。; 使用场景及目标:①在遥感影像中自动识别和提取特定地物目标(如车辆、建筑、道路、集装箱等);②掌握ENVI环境下深度学习模型的训练流程与关键参数设置(如Patch Size、Epochs、Class Weight等);③通过模型调优与结果反馈提升分类精度,实现高效自动化信息提取。; 阅读建议:建议结合实际遥感项目边学边练,重点关注标签数据制作、模型参数配置与结果后处理环节,充分利用ENVI Modeler进行自动化建模与参数优化,同时注意软硬件环境(特别是NVIDIA GPU)的配置要求以保障训练效率。
内容概要:本文系统阐述了企业新闻发稿在生成式引擎优化(GEO)时代下的全渠道策略与效果评估体系,涵盖当前企业传播面临的预算、资源、内容与效果评估四大挑战,并深入分析2025年新闻发稿行业五大趋势,包括AI驱动的智能化转型、精准化传播、首发内容价值提升、内容资产化及数据可视化。文章重点解析央媒、地方官媒、综合门户和自媒体四类媒体资源的特性、传播优势与发稿策略,提出基于内容适配性、时间节奏、话题设计的策略制定方法,并构建涵盖品牌价值、销售转化与GEO优化的多维评估框架。此外,结合“传声港”工具实操指南,提供AI智能投放、效果监测、自媒体管理与舆情应对的全流程解决方案,并针对科技、消费、B2B、区域品牌四大行业推出定制化发稿方案。; 适合人群:企业市场/公关负责人、品牌传播管理者、数字营销从业者及中小企业决策者,具备一定媒体传播经验并希望提升发稿效率与ROI的专业人士。; 使用场景及目标:①制定科学的新闻发稿策略,实现从“流量思维”向“价值思维”转型;②构建央媒定调、门户扩散、自媒体互动的立体化传播矩阵;③利用AI工具实现精准投放与GEO优化,提升品牌在AI搜索中的权威性与可见性;④通过数据驱动评估体系量化品牌影响力与销售转化效果。; 阅读建议:建议结合文中提供的实操清单、案例分析与工具指南进行系统学习,重点关注媒体适配性策略与GEO评估指标,在实际发稿中分阶段试点“AI+全渠道”组合策略,并定期复盘优化,以实现品牌传播的长期复利效应。
// 标准C++库头文件 #include <iostream> // 输入输出流 #include <fstream> // 文件流操作 #include <limits> // 数值限制 #include <string> // 字符串处理 #include <filesystem> // 文件系统操作 #include <unordered_map> // 无序映射容器 #include <list> // 链表容器 #include <utility> // 工具函数 #include <vector> // 向量容器 // Assimp库头文件 - 用于3D模型导入导出 #include <assimp/Importer.hpp> // 模型导入器 #include <assimp/Exporter.hpp> // 模型导出器 #include <assimp/mesh.h> // 网格数据结构 #include <assimp/postprocess.h> // 后处理选项 #include <assimp/scene.h> // 场景数据结构 // CGAL库头文件 - 用于计算几何和网格处理 #include <CGAL/Exact_predicates_inexact_constructions_kernel.h> // 精确谓词内核 #include <CGAL/Polygon_mesh_processing/bbox.h> // 包围盒计算 #include <CGAL/Polygon_mesh_processing/connected_components.h> // 连通分量 #include <CGAL/Polygon_mesh_processing/clip.h> // 网格裁剪 #include <CGAL/Polygon_mesh_processing/corefinement.h> // 核心细化 #include <CGAL/Polygon_mesh_processing/polygon_mesh_to_polygon_soup.h> // 网格转多边形汤 #include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h> // 多边形汤转网格 #include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h> // 修复多边形汤 #include <CGAL/Polygon_mesh_processing/orientation.h> // 方向处理 #include <CGAL/Polygon_mesh_processing/remesh.h> // 重新网格化 #include <CGAL/Surface_mesh.h> // 表面网格 #include <CGAL/Surface_mesh_parameterization/parameterize.h> // 参数化 #include <CGAL/Surface_mesh_parameterization/ARAP_parameterizer_3.h> // ARAP参数化器 #include <CGAL/Surface_mesh_parameterization/Mean_value_coordinates_parameterizer_3.h> // 均值坐标参数化器 #include <CGAL/Surface_mesh_parameterization/LSCM_parameterizer_3.h> // LSCM参数化器 #include <CGAL/Surface_mesh_parameterization/Discrete_authalic_parameterizer_3.h> // 离散等面积参数化器 #include <CGAL/Surface_mesh_parameterization/Barycentric_mapping_parameterizer_3.h> // 重心映射参数化器 #include <CGAL/Surface_mesh_parameterization/Discrete_conformal_map_parameterizer_3.h> // 离散共形映射参数化器 #include <CGAL/Surface_mesh_parameterization/Iterative_authalic_parameterizer_3.h> // 迭代等面积参数化器 #include <CGAL/Optimal_bounding_box/oriented_bounding_box.h> // 最优包围盒 // 使用标准命名空间 using namespace std; // 文件系统命名空间别名 namespace fs = std::filesystem; // CGAL库命名空间别名,简化代码 namespace PMP = CGAL::Polygon_mesh_processing; // 多边形网格处理 namespace SMP = CGAL::Surface_mesh_parameterization; // 表面网格参数化 // 几何内核类型定义 - 使用精确谓词内核,保证数值稳定性 typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel; //typedef CGAL::Simple_cartesian<double> Kernel; // 备选简单笛卡尔内核 typedef Kernel::Point_2 Point_2; // 2D点类型 typedef Kernel::Point_3 Point; // 3D点类型 typedef CGAL::Surface_mesh<Point> CGALMesh; // CGAL表面网格类型 typedef CGAL::SM_Vertex_index VertexIndex; // 顶点索引类型 //typedef CGALMesh::Property_map<VertexIndex, Kernel::Point_2> VertexUVPmap; // UV坐标属性映射 // 其他几何类型定义 typedef Kernel::Vector_3 Vector_3; // 3D向量 typedef Kernel::Plane_3 Plane_3; // 3D平面 typedef Kernel::Aff_transformation_3 Aff_transformation_3; // 仿射变换 namespace OB = CGAL::Optimal_bounding_box; // 最优包围盒命名空间 // 网格实体类型 - 包含网格、颜色和名称的元组 using MeshEntity = std::tuple<CGALMesh, aiColor3D, std::string>; /** * 剖切面访问者类 - 用于在网格裁剪过程中收集剖切面 * 这个类实现了CGAL的访问者模式,用于跟踪裁剪操作并生成剖切面 */ struct SectionVisitor { CGALMesh& section; // 存储生成的剖切面网格 std::map<CGALMesh::Vertex_index, CGALMesh::Vertex_index> vmap; // 顶点映射表,记录原网格顶点到剖切面顶点的映射 SectionVisitor(CGALMesh& sm) : section(sm) {} // 构造函数,传入剖切面网格引用 // 图论相关的类型定义,用于网格处理 typedef CGALMesh TriangleMesh; // 三角形网格类型 typedef boost::graph_traits<TriangleMesh> GT; // 图特征类型 typedef typename GT::face_descriptor face_descriptor; // 面描述符 typedef typename GT::halfedge_descriptor halfedge_descriptor; // 半边描述符 typedef typename GT::vertex_descriptor vertex_descriptor; // 顶点描述符 // face visitor functions void before_subface_creations(face_descriptor /*f_old*/, TriangleMesh&) {} void after_subface_creations(TriangleMesh&) {} void before_subface_created(TriangleMesh&) {} void after_subface_created(face_descriptor, TriangleMesh&) {} void before_face_copy(face_descriptor, const TriangleMesh&, TriangleMesh&) {} void after_face_copy(face_descriptor f_old, const TriangleMesh& tm_old, face_descriptor /*f_new*/, TriangleMesh& /*tm_new*/) { std::vector<CGALMesh::Vertex_index> vertices; for (auto v : vertices_around_face(tm_old.halfedge(f_old), tm_old)) { auto it = vmap.find(v); if (it != vmap.end()) { vertices.push_back(it->second); } } if (vertices.size() == 3) { section.add_face(vertices[0], vertices[1], vertices[2]); } } // edge visitor functions void before_edge_split(halfedge_descriptor /* h */, TriangleMesh& /* tm */) {} void edge_split(halfedge_descriptor /* hnew */, TriangleMesh& /* tm */) {} void after_edge_split() {} void add_retriangulation_edge(halfedge_descriptor /* h */, TriangleMesh& /* tm */) {} // edges added during split face retriangulation void before_edge_copy(halfedge_descriptor /*h_old*/, const TriangleMesh&, TriangleMesh&) {} void after_edge_copy(halfedge_descriptor /*h_old*/, const TriangleMesh&, halfedge_descriptor /* f_new */, TriangleMesh&) {} void before_edge_duplicated(halfedge_descriptor /*h_old*/, TriangleMesh&) {} // called before a patch border edge is duplicated void after_edge_duplicated(halfedge_descriptor /*h_old*/, halfedge_descriptor /* f_new */, TriangleMesh&) {} // called after a patch border edge is duplicated void intersection_edge_copy(halfedge_descriptor /* h_old1 */, const TriangleMesh& /* tm1 */, halfedge_descriptor /* h_old2 */, const TriangleMesh& /* tm2 */, halfedge_descriptor /* h_new */, TriangleMesh& /* tm_new */) {} // vertex visitor functions void new_vertex_added(std::size_t /*node_id*/, vertex_descriptor vh, const TriangleMesh& tm) { vmap[vh] = section.add_vertex(tm.point(vh)); } void intersection_point_detected(std::size_t /* node_id */, int /* sdim */, halfedge_descriptor /* principal_edge */, halfedge_descriptor /* additional_edge */, const TriangleMesh& /* tm1 */, const TriangleMesh& /* tm2 */, bool /* is_target_coplanar */, bool /* is_source_coplanar */) {} void before_vertex_copy(vertex_descriptor /*v_src*/, const TriangleMesh& /*tm_src*/, TriangleMesh& /*tm_tgt*/) {} void after_vertex_copy(vertex_descriptor v_src, const TriangleMesh& tm_src, vertex_descriptor /*v_tgt*/, TriangleMesh& /*tm_tgt*/) { vmap[v_src] = section.add_vertex(tm_src.point(v_src)); } // progress tracking void start_filtering_intersections() const {} void progress_filtering_intersections(double) const {} void end_filtering_intersections() const {} void start_triangulating_faces(std::size_t) const {} void triangulating_faces_step() const {} void end_triangulating_faces() const {} void start_handling_intersection_of_coplanar_faces(std::size_t) const {} void intersection_of_coplanar_faces_step() const {} void end_handling_intersection_of_coplanar_faces() const {} void start_handling_edge_face_intersections(std::size_t) const {} void edge_face_intersections_step() const {} void end_handling_edge_face_intersections() const {} void start_building_output() const {} void end_building_output() const {} // Required by Face_graph_output_builder void filter_coplanar_edges() const {} void detect_patches() const {} void classify_patches() const {} void classify_intersection_free_patches(const TriangleMesh&) const {} void out_of_place_operation(PMP::Corefinement::Boolean_operation_type) const {} void in_place_operation(PMP::Corefinement::Boolean_operation_type) const {} void in_place_operations(PMP::Corefinement::Boolean_operation_type, PMP::Corefinement::Boolean_operation_type) const {} }; /** * 读取剖切曲线文件 * @param pth 曲线文件路径 * @return 包含3D点坐标的向量 * * 功能:从文本文件中读取3D点坐标,每行包含x y z三个浮点数 * 文件格式:每行一个3D点,坐标用空格分隔 */ vector<Point> readCurve(const fs::path& pth) { std::vector<Point> curve; // 存储读取的曲线点 std::ifstream file(pth); // 打开文件流 // 检查文件是否成功打开 if (!file.is_open()) { std::cerr << "Failed to open file: " << pth << std::endl; return curve; } double x, y, z; // 临时存储坐标值 int pointCount = 0; // 记录读取的点数 // 逐行读取文件中的坐标 while (file >> x >> y >> z) { // 检查坐标值是否为NaN(非数字) if (std::isnan(x) || std::isnan(y) || std::isnan(z)) { std::cerr << "Invalid curve point: NaN detected" << std::endl; continue; // 跳过无效点 } curve.push_back(Point(x, y, z)); // 将有效点添加到曲线中 pointCount++; } file.close(); // 关闭文件 std::cout << "Read " << pointCount << " points from curve file" << std::endl; // 检查是否读取到有效点 if (curve.empty()) { std::cerr << "Warning: No valid points read from curve file!" << std::endl; } return curve; } /** * 生成剖切面方法1 - 创建垂直延伸的平面 * @param curve 输入曲线点集 * @return 生成的剖切面网格 * * 算法:将曲线上的每个点沿Z轴正负方向延伸,形成垂直的平面 * 每个曲线段生成两个三角形,形成连续的垂直平面 */ CGALMesh generateSurface1(const vector<Point>& curve) { CGALMesh surface; // 创建空网格 double extensionDistance = 5000; // 延伸距离,沿Z轴正负方向各延伸5000单位 // 检查输入曲线是否为空 if (curve.empty()) { std::cerr << "Error: Cannot generate surface from empty curve!" << std::endl; return surface; } std::cout << "Generating surface from " << curve.size() << " curve points" << std::endl; // 为每个曲线点创建两个顶点(上下延伸) for (const auto& p : curve) { Point p1(p.x(), p.y(), p.z() - extensionDistance); // 下方顶点 Point p2(p.x(), p.y(), p.z() + extensionDistance); // 上方顶点 surface.add_vertex(p1); surface.add_vertex(p2); } // 连接相邻点形成三角形面片 for (size_t i = 0; i < curve.size() - 1; ++i) { size_t baseIndex = i * 2; // 当前段的顶点起始索引 // 第一个三角形:当前下顶点 -> 下一上顶点 -> 当前上顶点 surface.add_face( CGALMesh::Vertex_index(baseIndex), CGALMesh::Vertex_index(baseIndex + 1), CGALMesh::Vertex_index(baseIndex + 2) ); // 第二个三角形:当前上顶点 -> 下一上顶点 -> 下一上顶点 surface.add_face( CGALMesh::Vertex_index(baseIndex + 1), CGALMesh::Vertex_index(baseIndex + 3), CGALMesh::Vertex_index(baseIndex + 2) ); } std::cout << "Generated surface with " << surface.number_of_vertices() << " vertices and " << surface.number_of_faces() << " faces" << std::endl; return surface; } /** * 生成剖切面方法2 - 创建更复杂的曲面结构 * @param curve 输入曲线点集 * @return 生成的剖切面网格 * * 算法:为每个曲线点创建三个顶点(下、中、上),形成更丰富的曲面结构 * 每个曲线段生成四个三角形,创建更平滑的曲面过渡 */ CGALMesh generateSurface2(const std::vector<Point>& curve) { CGALMesh surface; // 创建空网格 double extensionDistance = 4000; // 延伸距离 // 为每个曲线点创建三个顶点(下、中、上) for (const auto& p : curve) { Point p_down(p.x(), p.y(), p.z()); // 下方顶点(原始高度) Point p_original(p.x(), p.y(), p.z() + 2000); // 中间顶点(偏移2000) Point p_up(p.x(), p.y(), p.z() + extensionDistance); // 上方顶点(最大高度) surface.add_vertex(p_down); surface.add_vertex(p_original); surface.add_vertex(p_up); } // 连接相邻点形成四个三角形面片(确保逆时针顺序) for (size_t i = 0; i < curve.size() - 1; ++i) { // 当前点和下一个点的顶点索引 size_t curr_base = i * 3; // 当前段的顶点起始索引 size_t next_base = (i + 1) * 3; // 下一段的顶点起始索引 // 连接两个相邻截面形成四个三角形 // 三角形1:当前下 → 下一原 → 当前原 surface.add_face( CGALMesh::Vertex_index(curr_base), CGALMesh::Vertex_index(next_base + 1), CGALMesh::Vertex_index(curr_base + 1) ); // 三角形2:当前原 → 下一原 → 下一上 surface.add_face( CGALMesh::Vertex_index(curr_base + 1), CGALMesh::Vertex_index(next_base + 1), CGALMesh::Vertex_index(next_base + 2) ); // 三角形3:当前下 → 下一原 → 下一上 surface.add_face( CGALMesh::Vertex_index(curr_base), CGALMesh::Vertex_index(next_base + 1), CGALMesh::Vertex_index(next_base + 2) ); // 三角形4:当前下 → 下一上 → 当前上 surface.add_face( CGALMesh::Vertex_index(curr_base), CGALMesh::Vertex_index(next_base + 2), CGALMesh::Vertex_index(curr_base + 2) ); } /* 注释掉的网格修复代码 std::vector<Point> points; std::vector<std::vector<size_t>> polygons; PMP::polygon_mesh_to_polygon_soup(surface, points, polygons); PMP::repair_polygon_soup(points, polygons); PMP::orient_polygon_soup(points, polygons); if (PMP::is_polygon_soup_a_polygon_mesh(polygons)) { PMP::polygon_soup_to_polygon_mesh(points, polygons, surface); }*/ return surface; } /** * 计算网格的包围盒 * @param mesh 输入网格 * @return 网格的3D包围盒 * * 功能:遍历网格的所有顶点,计算整个网格的边界框 */ CGAL::Bbox_3 compute_mesh_bbox(const CGALMesh& mesh) { CGAL::Bbox_3 bbox; // 创建空包围盒 for (auto v : mesh.vertices()) { // 遍历所有顶点 const auto& p = mesh.point(v); // 获取顶点坐标 bbox += p.bbox(); // 将顶点添加到包围盒中 } return bbox; } /** * 计算边界环的包围盒 * @param mesh 输入网格 * @param border_halfedge 边界环的起始半边 * @return 边界环的3D包围盒 * * 功能:沿着边界环遍历所有顶点,计算边界环的边界框 */ CGAL::Bbox_3 compute_border_cycle_bbox(const CGALMesh& mesh, CGALMesh::Halfedge_index border_halfedge) { CGAL::Bbox_3 border_bbox; // 创建边界环包围盒 CGALMesh::Halfedge_index h = border_halfedge; // 从起始半边开始 // 沿着边界环遍历所有半边 do { auto v = mesh.target(h); // 获取当前半边目标顶点 const auto& p = mesh.point(v); // 获取顶点坐标 border_bbox += p.bbox(); // 将顶点添加到边界环包围盒中 h = mesh.next(h); // 移动到下一个半边 } while (h != border_halfedge); // 直到回到起始半边 return border_bbox; } /** * 判断网格是否为外边界封闭网格 * @param mesh 输入网格 * @return 如果是外边界封闭网格返回true,否则返回false * * 功能:判断网格是否只有一个大的外边界环,用于识别需要跳过的网格 * 判断标准: * 1. 网格不能是完全封闭的 * 2. 只能有一个边界环 * 3. 边界环必须足够大(至少20条边) * 4. 边界环的包围盒必须覆盖网格包围盒的90%以上 */ bool is_outer_closed_boundary(const CGALMesh& mesh) { // 如果网格完全封闭,则不是外边界 if (CGAL::is_closed(mesh)) return false; // 获取所有边界环 std::vector<CGALMesh::Halfedge_index> border_cycles; CGAL::Polygon_mesh_processing::extract_boundary_cycles(mesh, std::back_inserter(border_cycles)); // 仅允许单一边界环(排除有多个洞的网格) if (border_cycles.size() != 1) return false; // 检查边界环长度(排除小孔洞) CGALMesh::Halfedge_index border_halfedge = border_cycles.front(); std::size_t edge_count = 0; CGALMesh::Halfedge_index start = border_halfedge; do { edge_count++; border_halfedge = mesh.next(border_halfedge); } while (border_halfedge != start); // 边界环必须足够大(至少20条边) const std::size_t MIN_BORDER_EDGES = 20; if (edge_count < MIN_BORDER_EDGES) return false; // 包围盒验证 - 检查边界环是否覆盖了网格的大部分区域 const CGAL::Bbox_3 mesh_bbox = compute_mesh_bbox(mesh); // 计算整个网格的包围盒 const CGAL::Bbox_3 border_bbox = compute_border_cycle_bbox(mesh, start); // 计算边界环的包围盒 // 计算包围盒在各轴上的覆盖比例 const double x_ratio = (border_bbox.xmax() - border_bbox.xmin()) / (mesh_bbox.xmax() - mesh_bbox.xmin()); const double y_ratio = (border_bbox.ymax() - border_bbox.ymin()) / (mesh_bbox.ymax() - mesh_bbox.ymin()); const double z_ratio = (border_bbox.zmax() - border_bbox.zmin()) / (mesh_bbox.zmax() - mesh_bbox.zmin()); // 设定阈值:边界环包围盒需覆盖整体包围盒的至少90% const double BBOX_COVERAGE_THRESHOLD = 0.9; if (x_ratio < BBOX_COVERAGE_THRESHOLD || y_ratio < BBOX_COVERAGE_THRESHOLD || z_ratio < BBOX_COVERAGE_THRESHOLD) { return false; } return true; // 满足所有条件,是外边界封闭网格 } bool AssimpFormatId(const Assimp::Exporter& exporter, const fs::path& filepath, std::string& formatId, bool binary) { std::string fpstr(filepath.string()); size_t ipos = fpstr.find_last_of('.'); formatId = fpstr.substr(ipos + 1); bool findFormat = false; if (formatId == "gltf" || formatId == "glb") { findFormat = true; formatId += "2"; } else { for (size_t i = 0; i < exporter.GetExportFormatCount(); i++) { const aiExportFormatDesc* desc = exporter.GetExportFormatDescription(i); if (strstr(desc->id, formatId.c_str()) != NULL) { findFormat = true; if (binary) { if (strstr(desc->id, "b") != NULL) { formatId = desc->id; break; } } else break; } } } return findFormat; } /** * 将Assimp网格转换为CGAL网格 * @param imh 输入的Assimp网格 * @param omh 输出的CGAL网格 * @param name 网格名称 * @return 转换成功返回true,失败返回false * * 功能:将Assimp库的网格数据结构转换为CGAL库的网格数据结构 * 包括顶点坐标和面片索引的转换,并进行网格修复和方向调整 */ static bool AiMeshToCgalMesh(const aiMesh& imh, CGALMesh& omh, std::string& name) { // 检查网格是否有效(至少3个顶点和1个面) if (imh.mNumVertices < 3 || imh.mNumFaces < 1) return false; // 获取网格名称 if (imh.mName.length > 0) { name = imh.mName.data; } // 准备顶点和面片数据 std::vector<Point> points; // 存储顶点坐标 std::vector<std::vector<size_t>> polygons; // 存储面片索引 // 转换顶点坐标 points.reserve(imh.mNumVertices); for (uint32_t i = 0; i < imh.mNumVertices; i++) { const aiVector3D& vet = imh.mVertices[i]; // 获取Assimp顶点 points.push_back(Point((double)vet.x, (double)vet.y, (double)vet.z)); // 转换为CGAL点 } // 转换面片索引 polygons.reserve(imh.mNumFaces); for (uint32_t i = 0; i < imh.mNumFaces; i++) { aiFace face = imh.mFaces[i]; // 获取Assimp面片 uint32_t* indices = face.mIndices; // 获取面片顶点索引 polygons.push_back({ (size_t)indices[0], (size_t)indices[1], (size_t)indices[2] }); // 转换为CGAL面片 } // 修复多边形汤(处理重复顶点、退化面片等) PMP::repair_polygon_soup(points, polygons); // 调整多边形汤的方向(确保面片法向量一致) PMP::orient_polygon_soup(points, polygons); // 检查是否可以转换为多边形网格 if (PMP::is_polygon_soup_a_polygon_mesh(polygons)) { PMP::polygon_soup_to_polygon_mesh(points, polygons, omh); // 转换为CGAL网格 return true; } return false; // 转换失败 } static bool CgalMeshToAiMesh(const CGALMesh& imh, const std::string& name, aiMesh& omh) { if (imh.is_empty() || imh.number_of_faces() == 0) return false; omh.mPrimitiveTypes = aiPrimitiveType_TRIANGLE; omh.mNumVertices = imh.number_of_vertices(); omh.mVertices = new aiVector3D[omh.mNumVertices]; std::unordered_map<VertexIndex, uint32_t> vimap; //! avoid order problem auto uvmap = imh.property_map<VertexIndex, Kernel::Point_2>("v:uv"); uint32_t vidx = 0; if (uvmap.second) { omh.mTextureCoords[0] = new aiVector3D[omh.mNumVertices]; omh.mNumUVComponents[0] = 2; uint32_t vidx = 0; for (const auto vd : CGAL::vertices(imh)) { const auto& point = imh.point(vd); omh.mVertices[vidx] = aiVector3D(static_cast<ai_real>(point.x()), static_cast<ai_real>(point.y()), static_cast<ai_real>(point.z())); const auto& uv = boost::get(uvmap.first,vd); omh.mTextureCoords[0][vidx] = aiVector3D(static_cast<ai_real>(uv.x()), static_cast<ai_real>(uv.y()), 0.0); vimap[vd] = vidx++; } } else { omh.mTextureCoords[0] = nullptr; omh.mNumUVComponents[0] = 0; uint32_t vidx = 0; for (const auto vd : CGAL::vertices(imh)) { const auto& point = imh.point(vd); omh.mVertices[vidx] = aiVector3D(static_cast<ai_real>(point.x()), static_cast<ai_real>(point.y()), static_cast<ai_real>(point.z())); vimap[vd] = vidx++; } } omh.mNumFaces = imh.number_of_faces(); omh.mFaces = new aiFace[omh.mNumFaces]; uint32_t fidx = 0; for (const auto& itr : imh.faces()) { CGAL::SM_Halfedge_index heidx = imh.halfedge(itr); const auto& fvids = imh.vertices_around_face(heidx); auto trif = &omh.mFaces[fidx]; trif->mIndices = new uint32_t[3]{ vimap[*fvids.begin()], vimap[*(++fvids.begin())], vimap[*(--fvids.end())] }; trif->mNumIndices = 3; fidx++; } omh.mName = name; return true; } static bool ImportMeshes(const fs::path& pth, std::vector<MeshEntity>& mshes) { Assimp::Importer ipt; uint32_t flags = aiProcess_Triangulate | aiProcess_JoinIdenticalVertices; // read file via ASSIMP const aiScene* scene = ipt.ReadFile(pth.u8string(), flags); // check for errors if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) // if is Not Zero { std::cerr << "ERROR::ASSIMP:: " << ipt.GetErrorString() << std::endl; return false; } aiColor3D clr; std::vector<aiColor3D> cvec; for (uint32_t i = 0; i < scene->mNumMaterials; i++) { auto mat = scene->mMaterials[i]; if (mat->Get(AI_MATKEY_BASE_COLOR, clr) != aiReturn_SUCCESS) { mat->Get(AI_MATKEY_COLOR_DIFFUSE, clr); } cvec.push_back(clr); } for (uint32_t i = 0; i < scene->mNumMeshes; i++) { auto imsh = scene->mMeshes[i]; CGALMesh omsh; std::string name; if (AiMeshToCgalMesh(*imsh, omsh, name)) { const auto clr = cvec[imsh->mMaterialIndex]; mshes.push_back({ std::move(omsh), clr, name }); } } return true; } static bool ExportMeshes(const fs::path& pth, const std::vector<MeshEntity>& items, bool texture) { if (items.empty()) { std::cerr << "Warning: No meshes to export!" << std::endl; return false; } std::cout << "Exporting " << items.size() << " meshes to: " << pth << std::endl; int midx = 0; std::vector<aiMesh*> meshes; std::vector<aiMaterial*> materials; for (const auto& item : items) { const auto& mesh = std::get<CGALMesh>(item); const auto& name = std::get<std::string>(item); const auto& color = std::get<aiColor3D>(item); std::cout << "Processing mesh '" << name << "' with " << mesh.number_of_vertices() << " vertices and " << mesh.number_of_faces() << " faces" << std::endl; if (mesh.is_empty()) { std::cerr << "Warning: Mesh '" << name << "' is empty, skipping" << std::endl; continue; } aiMesh* omsh = new aiMesh; if (CgalMeshToAiMesh(mesh, name, *omsh)) { aiMaterial* mat = new aiMaterial; if (texture) { mat->AddProperty(new aiString(name + ".png"), AI_MATKEY_TEXTURE_DIFFUSE(0)); } else { mat->AddProperty(new aiColor3D(color), 1, AI_MATKEY_COLOR_DIFFUSE); } mat->AddProperty(new aiString(name), AI_MATKEY_NAME); omsh->mMaterialIndex = midx; meshes.push_back(omsh); materials.push_back(mat); midx++; } else { std::cerr << "Warning: Failed to convert mesh '" << name << "' to Assimp format" << std::endl; delete omsh; } } aiScene* scene = new aiScene(); scene->mNumMeshes = (uint32_t)meshes.size(); scene->mMeshes = new aiMesh * [scene->mNumMeshes]; scene->mNumMaterials = (uint32_t)materials.size(); scene->mMaterials = new aiMaterial * [scene->mNumMaterials]; #ifdef _WIN32 memcpy_s(scene->mMeshes, scene->mNumMeshes * sizeof(aiMesh*), meshes.data(), scene->mNumMeshes * sizeof(aiMesh*)); memcpy_s(scene->mMaterials, scene->mNumMaterials * sizeof(aiMaterial*), materials.data(), scene->mNumMaterials * sizeof(aiMaterial*)); #elif defined __GNUC__ memcpy(scene->mMeshes, meshes.data(), scene->mNumMeshes * sizeof(aiMesh*)); memcpy(scene->mMaterials, materials.data(), scene->mNumMaterials * sizeof(aiMaterial*)); #endif aiNode* node = new aiNode; node->mNumMeshes = (uint32_t)meshes.size(); node->mMeshes = new uint32_t[node->mNumMeshes]; for (uint32_t i = 0; i < node->mNumMeshes; i++) { node->mMeshes[i] = i; } scene->mRootNode = node; std::string formatId; Assimp::Exporter exporter; int flags = aiProcess_GenSmoothNormals; if (!AssimpFormatId(exporter, pth, formatId, false)) return false; return exporter.Export(scene, formatId, pth.u8string(), flags) == aiReturn_SUCCESS; } static bool fnRepair(CGALMesh& mesh) { if (mesh.is_empty()) return false; CGAL::Polygon_mesh_processing::stitch_borders(mesh); std::vector<CGALMesh::Halfedge_index> borders; PMP::extract_boundary_cycles(mesh, std::back_inserter(borders)); if (!borders.empty()) { std::vector<CGALMesh::Face_index> new_faces; CGAL::Polygon_mesh_processing::triangulate_hole( mesh, borders[0], CGAL::parameters::face_output_iterator(std::back_inserter(new_faces))); } return true; } static bool SectionClipping(CGALMesh& msh, CGALMesh& clp, CGALMesh& section) { if (msh.is_empty()) { std::cerr << "Error: Input mesh is empty" << std::endl; return false; } if (clp.is_empty()) { std::cerr << "Error: Clip surface is empty" << std::endl; return false; } std::cout << "Input mesh has " << msh.number_of_vertices() << " vertices and " << msh.number_of_faces() << " faces" << std::endl; std::cout << "Clip surface has " << clp.number_of_vertices() << " vertices and " << clp.number_of_faces() << " faces" << std::endl; //auto fnRepair = [](CGALMesh& mesh) { // CGAL::Polygon_mesh_processing::stitch_borders(mesh); // std::vector<CGALMesh::Halfedge_index> borders; // //PMP::extract_boundary_cycles(mesh, std::back_inserter(borders)); // for (auto h : mesh.halfedges()) { // if (mesh.is_border(h)) borders.push_back(h); // } // if (!borders.empty()) { // std::vector<CGALMesh::Face_index> new_faces; // CGAL::Polygon_mesh_processing::triangulate_hole( // mesh, borders[0], CGAL::parameters::face_output_iterator(std::back_inserter(new_faces))); // } // }; std::vector<CGALMesh> meshes; PMP::split_connected_components(msh, meshes); std::cout << "Split into " << meshes.size() << " connected components" << std::endl; bool status = true; int processedComponents = 0; for (auto& spt : meshes) { size_t nf = spt.number_of_faces(); if (nf < 2) { std::cout << "Skipping component with only " << nf << " faces" << std::endl; continue; } bool is_out = is_outer_closed_boundary(spt); if (is_out) { std::cout << "Skipping outer boundary component" << std::endl; continue; } SectionVisitor visitor(section); if (!CGAL::is_closed(spt)) { std::cout << "Repairing non-closed component" << std::endl; fnRepair(spt); } if (!CGAL::is_closed(spt)) { std::cout << "Component still not closed after repair, skipping" << std::endl; continue; } std::cout << "Clipping component " << processedComponents++ << " with " << spt.number_of_vertices() << " vertices and " << spt.number_of_faces() << " faces" << std::endl; bool clipResult = PMP::clip(spt, clp, CGAL::parameters::clip_volume(true).visitor(visitor)); status = status && clipResult; if (!clipResult) { std::cerr << "Warning: Clip operation failed for component " << processedComponents - 1 << std::endl; } } std::cout << "Section mesh has " << section.number_of_vertices() << " vertices and " << section.number_of_faces() << " faces" << std::endl; return status; } static bool GenerateUVCoord(CGALMesh& target) { if (target.is_empty()) return false; std::vector<Point> points; std::vector<std::vector<size_t>> polygons; PMP::polygon_mesh_to_polygon_soup(target, points, polygons); PMP::repair_polygon_soup(points, polygons); PMP::orient_polygon_soup(points, polygons); if (PMP::is_polygon_soup_a_polygon_mesh(polygons)) { PMP::polygon_soup_to_polygon_mesh(points, polygons, target); } auto uvmap = target.add_property_map<VertexIndex, Kernel::Point_2> ("v:uv", Kernel::Point_2(0.0, 0.0)).first; /*CGALMesh::Halfedge_index bh; for (auto h : target.halfedges()) { if (target.is_border(h)) { bh = h; break; } }*/ std::vector<CGALMesh::Halfedge_index> bhs; PMP::extract_boundary_cycles(target, std::back_inserter(bhs)); //SMP::ARAP_parameterizer_3<CGALMesh> parameterizer; SMP::LSCM_parameterizer_3<CGALMesh> parameterizer; //SMP::Mean_value_coordinates_parameterizer_3<CGALMesh> parameterizer; //SMP::Discrete_authalic_parameterizer_3<CGALMesh> parameterizer; //SMP::Barycentric_mapping_parameterizer_3<CGALMesh> parameterizer; //SMP::Discrete_conformal_map_parameterizer_3<CGALMesh> parameterizer; //SMP::Iterative_authalic_parameterizer_3<CGALMesh> parameterizer; //const unsigned int iterations = 15; for (auto& bh : bhs) { auto ecode = SMP::parameterize(target, parameterizer, bh, uvmap); if (ecode != SMP::OK) { cout << "Parameterize failed" << ecode << endl; return false; } } aiVector2D min(static_cast<ai_real>(std::numeric_limits<double>::max())); aiVector2D max(static_cast<ai_real>(-std::numeric_limits<double>::max())); for (auto pnt : uvmap) { min.x = std::min(static_cast<ai_real>(min.x), static_cast<ai_real>(pnt.x())); min.y = std::min(static_cast<ai_real>(min.y), static_cast<ai_real>(pnt.y())); max.x = std::max(static_cast<ai_real>(max.x), static_cast<ai_real>(pnt.x())); max.y = std::max(static_cast<ai_real>(max.y), static_cast<ai_real>(pnt.y())); } auto extent = max - min; double length = std::max(static_cast<double>(extent.x), static_cast<double>(extent.y)); double step = 1.0 / length; for (const auto vd : CGAL::vertices(target)) { auto pnt = get(uvmap, vd); double u = (static_cast<double>(pnt.x()) - static_cast<double>(min.x)) * step; double v = (static_cast<double>(pnt.y()) - static_cast<double>(min.y)) * step; put(uvmap, vd, Kernel::Point_2(u - std::floor(u), v - std::floor(v))); } return true; } static bool SaveAfterClipOriMesh_UVCoord(const MeshEntity& input, CGALMesh& clp, vector<MeshEntity>& out, bool texture) { const CGALMesh& msh = get<0>(input); if (msh.is_empty()) return false; std::vector<CGALMesh> meshes; PMP::split_connected_components(msh, meshes); for (auto& spt : meshes) { size_t nf = spt.number_of_faces(); if (nf < 2) continue; bool is_out = is_outer_closed_boundary(spt); if (is_out) continue; if (!CGAL::is_closed(spt)) { fnRepair(spt); } if (!CGAL::is_closed(spt)) { continue; } PMP::clip(spt, clp, CGAL::parameters::clip_volume(false)); if (texture) { GenerateUVCoord(spt); } out.push_back({ spt, get<1>(input), get<2>(input) }); } return true; } /** * 主函数 - 3D模型剖切程序入口 * * 程序功能: * 1. 读取剖切曲线文件 * 2. 生成剖切面 * 3. 读取3D模型 * 4. 执行剖切操作 * 5. 导出剖切面和剖切后的模型 * * 命令行参数: * argv[1] - 剖切曲线文件路径 * argv[2] - 输入模型文件路径 * argv[3] - 输出剖切面文件路径 * argv[4] - 输出剖切后模型文件路径 * argv[5] - 可选参数 "withTex" 表示生成纹理坐标 */ int main(int argc, char** argv) { // 检查命令行参数数量 if (argc < 5) { std::cerr << "Usage: " << argv[0] << " <clip_line_file> <model_file> <output_section> <output_clipped> [withTex]" << std::endl; std::cerr << "参数说明:" << std::endl; std::cerr << " clip_line_file: 剖切曲线文件(包含3D点坐标)" << std::endl; std::cerr << " model_file: 输入3D模型文件" << std::endl; std::cerr << " output_section: 输出剖切面文件" << std::endl; std::cerr << " output_clipped: 输出剖切后模型文件" << std::endl; std::cerr << " withTex: 可选参数,生成纹理坐标" << std::endl; return 0; } // 解析命令行参数 fs::path ClipLine_inpath(argv[1]); // 剖切曲线文件路径 fs::path Model_inpath(argv[2]); // 输入模型文件路径 fs::path outpath1(argv[3]); // 输出剖切面文件路径 fs::path outpath2(argv[4]); // 输出剖切后模型文件路径 // 检查是否启用纹理坐标生成 bool withTex = (argc > 5 && strcmp(argv[5], "withTex") == 0); //bool withTex = true; // 可以强制启用纹理坐标生成 // 步骤1:读取剖切曲线 std::cout << "Reading clip line from: " << ClipLine_inpath << std::endl; vector<Point> curve = readCurve(ClipLine_inpath); // 检查曲线是否有效 if (curve.empty()) { std::cerr << "Error: No valid curve points found. Cannot proceed." << std::endl; return 1; } // 步骤2:生成剖切面 std::cout << "Generating clip surface..." << std::endl; CGALMesh clip = generateSurface1(curve); // 使用方法1生成剖切面 // 检查剖切面是否有效 if (clip.is_empty()) { std::cerr << "Error: Generated clip surface is empty!" << std::endl; return 1; } // 步骤3:读取输入模型 std::cout << "Reading model from: " << Model_inpath << std::endl; std::vector<MeshEntity> ims; // 存储导入的网格实体 if (!ImportMeshes(Model_inpath, ims)) { std::cerr << "Error: Failed to import model!" << std::endl; return 1; } std::cout << "Imported " << ims.size() << " mesh entities" << std::endl; // 步骤4:执行剖切操作 std::vector<MeshEntity> oms, ori; // oms存储剖切面,ori存储剖切后的模型 // 处理每个网格实体,生成剖切面 for (const auto& item : ims) { CGALMesh msh = get<0>(item); // 获取网格 CGALMesh clp = clip; // 复制剖切面 CGALMesh outm; // 输出剖切面 std::cout << "Processing mesh: " << std::get<2>(item) << std::endl; // 执行剖切操作 if (SectionClipping(msh, clp, outm)) { if (!outm.is_empty()) { std::cout << "Generated section with " << outm.number_of_vertices() << " vertices and " << outm.number_of_faces() << " faces" << std::endl; // 如果需要,生成纹理坐标 if (withTex) { GenerateUVCoord(outm); } // 将剖切面添加到输出列表 oms.push_back({ outm, std::get<1>(item), std::get<2>(item) }); } else { std::cout << "Warning: Generated section is empty" << std::endl; } } else { std::cout << "Warning: Section clipping failed" << std::endl; } } // 处理每个网格实体,生成剖切后的模型 for (const auto& item : ims) { CGALMesh clp = clip; SaveAfterClipOriMesh_UVCoord(item, clp, ori, false); } /* 注释掉的合并代码 std::vector<MeshEntity> all; all.reserve(oms.size() + ori.size()); all.insert(all.end(), oms.begin(), oms.end()); all.insert(all.end(), ori.begin(), ori.end()); */ // 步骤5:导出结果 std::cout << "Exporting " << oms.size() << " section meshes to: " << outpath1 << std::endl; ExportMeshes(outpath1, oms, withTex); std::cout << "Exporting " << ori.size() << " clipped meshes to: " << outpath2 << std::endl; ExportMeshes(outpath2, ori, false); return 1; // 程序成功完成 }
09-20
### 代码功能概述 使用标准C++库、Assimp库和CGAL库编写的3D模型剖切代码,主要涉及读取曲线、生成剖切面、模型导入导出、剖切操作以及UV坐标生成等功能。标准C++库提供基础的数据类型和算法支持;Assimp库用于模型的导入和导出;CGAL库则在计算几何方面发挥作用,例如处理剖切面的生成和剖切操作。 ### 代码理解要点 1. **读取曲线**:标准C++库可用于文件读取,将曲线数据从文件中读入到合适的数据结构中。如使用`std::ifstream`读取文本文件,存储曲线的点坐标等信息。 ```cpp #include <fstream> #include <vector> #include <iostream> struct Point { double x, y, z; }; std::vector<Point> readCurve(const std::string& filename) { std::vector<Point> curve; std::ifstream file(filename); if (file.is_open()) { double x, y, z; while (file >> x >> y >> z) { curve.push_back({x, y, z}); } file.close(); } return curve; } ``` 2. **生成剖切面**:CGAL库提供了丰富的计算几何算法,可根据读取的曲线数据生成剖切面。例如,若曲线是封闭的,可以使用CGAL的多边形相关类来表示剖切面。 ```cpp #include <CGAL/Exact_predicates_inexact_constructions_kernel.h> #include <CGAL/Polygon_2.h> typedef CGAL::Exact_predicates_inexact_constructions_kernel K; typedef CGAL::Polygon_2<K> Polygon_2; Polygon_2 generateCutPlane(const std::vector<Point>& curve) { Polygon_2 plane; for (const auto& p : curve) { plane.push_back(K::Point_2(p.x, p.y)); } return plane; } ``` 3. **模型导入导出**:Assimp库可实现多种3D模型格式的导入和导出。通过`aiImportFile`导入模型,`aiExportScene`导出模型。 ```cpp #include <assimp/Importer.hpp> #include <assimp/scene.h> #include <assimp/postprocess.h> #include <assimp/Exporter.hpp> const aiScene* importModel(const std::string& filename) { Assimp::Importer importer; const aiScene* scene = importer.ReadFile(filename, aiProcess_Triangulate | aiProcess_FlipUVs); if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE ||!scene->mRootNode) { std::cerr << "Error importing model: " << importer.GetErrorString() << std::endl; return nullptr; } return scene; } bool exportModel(const aiScene* scene, const std::string& filename, const std::string& format) { Assimp::Exporter exporter; if (exporter.Export(scene, format.c_str(), filename) != AI_SUCCESS) { std::cerr << "Error exporting model: " << exporter.GetErrorString() << std::endl; return false; } return true; } ``` 4. **剖切操作**:结合CGAL和Assimp的数据结构,对导入的模型进行剖切。这可能涉及到遍历模型的面和顶点,判断其与剖切面的位置关系。 ```cpp // 简单示意,实际剖切逻辑更复杂 void cutModel(const aiScene* scene, const Polygon_2& plane) { // 遍历场景中的每个网格 for (unsigned int i = 0; i < scene->mNumMeshes; ++i) { aiMesh* mesh = scene->mMeshes[i]; // 处理网格的顶点和面 for (unsigned int j = 0; j < mesh->mNumFaces; ++j) { aiFace& face = mesh->mFaces[j]; // 判断面与剖切面的关系 // ... } } } ``` 5. **UV坐标生成**:在模型剖切后,可能需要重新生成UV坐标。可以使用标准C++库和CGAL库的算法,根据剖切后的模型顶点位置计算UV坐标。 ```cpp // 简单示意,实际UV生成逻辑更复杂 void generateUVCoordinates(aiMesh* mesh) { for (unsigned int i = 0; i < mesh->mNumVertices; ++i) { aiVector3D& vertex = mesh->mVertices[i]; // 计算UV坐标 float u = vertex.x; float v = vertex.y; mesh->mTextureCoords[0][i] = aiVector3D(u, v, 0.0f); } } ``` ### 代码优化建议 1. **性能优化**:在读取曲线和模型时,可使用多线程并行处理,提高读取效率。在剖切操作中,使用空间划分算法(如八叉树)减少不必要的计算。 2. **内存管理**:确保在使用完Assimp的`aiScene`和`aiMesh`等对象后,及时释放内存,避免内存泄漏。 3. **代码结构优化**:将不同功能的代码封装成独立的函数或类,提高代码的可读性和可维护性。 ### 潜在问题查找 1. **数据精度问题**:CGAL库使用精确的几何算法,但在与Assimp库交互时,可能会因为数据精度的差异导致计算结果不准确。 2. **文件格式兼容性**:Assimp库虽然支持多种文件格式,但某些特殊格式可能存在兼容性问题,需要进行额外的处理。 3. **边界情况处理**:在剖切操作中,需要考虑模型与剖切面的各种边界情况,如相切、相交于顶点等,避免出现错误结果。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值