feat(v3.8): 2D projection views + section + DXF + dimensions + G2 curvature
CI / Build & Test (push) Failing after 41s
CI / Release Build (push) Failing after 35s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

This commit is contained in:
茂之钳
2026-07-24 15:23:30 +00:00
parent 6779b45f27
commit d2c628215d
5 changed files with 446 additions and 0 deletions
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "vde/brep/brep.h"
#include "vde/core/point.h"
#include <vector>
#include <string>
namespace vde::brep {
/// 2D line segment for drawing output
struct DrawSegment {
double x1, y1, x2, y2;
bool hidden = false;
};
/// Projection view of a B-Rep body
struct ProjectionView {
std::string name;
std::vector<DrawSegment> segments;
std::vector<DrawSegment> hidden_lines;
double scale = 1.0;
[[nodiscard]] size_t total_segments() const { return segments.size() + hidden_lines.size(); }
};
/// Generate orthographic projection views (front, top, right)
[[nodiscard]] std::vector<ProjectionView> generate_views(const BrepModel& body);
/// Generate a section view
[[nodiscard]] ProjectionView section_view(const BrepModel& body,
const core::Point3D& point, const core::Vector3D& normal);
/// Export to DXF R12 format
bool export_dxf(const std::string& filepath, const std::vector<ProjectionView>& views);
/// Dimension type
enum class DimType { Linear, Radius, Diameter };
/// A dimension annotation
struct Dimension {
DimType type = DimType::Linear;
double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
std::string text;
};
/// Auto-generate dimensions for a view
[[nodiscard]] std::vector<Dimension> auto_dimension(const ProjectionView& view);
} // namespace vde::brep
+1
View File
@@ -145,6 +145,7 @@ add_library(vde::engine ALIAS vde)
add_library(vde_brep STATIC add_library(vde_brep STATIC
brep/brep.cpp brep/brep.cpp
brep/modeling.cpp brep/modeling.cpp
brep/brep_drawing.cpp
brep/step_export.cpp brep/step_export.cpp
brep/step_import.cpp brep/step_import.cpp
brep/iges_import.cpp brep/iges_import.cpp
+152
View File
@@ -0,0 +1,152 @@
#include "vde/brep/brep_drawing.h"
#include <fstream>
#include <algorithm>
#include <cmath>
namespace vde::brep {
using core::Point3D;
using core::Vector3D;
namespace {
// Project a 3D point to 2D based on view direction
std::pair<double,double> project(const Point3D& p, const Vector3D& dir) {
// Front view (look down -Z): project to XY
if (std::abs(dir.z() + 1.0) < 0.1) return {p.x(), p.y()};
// Top view (look down -Y): project to XZ, map Z→Y
if (std::abs(dir.y() + 1.0) < 0.1) return {p.x(), -p.z()};
// Right view (look from +X): project to YZ, map Z→X, Y→Y
if (std::abs(dir.x() - 1.0) < 0.1) return {p.z(), p.y()};
// Default: XY plane
return {p.x(), p.y()};
}
void add_edge_segments(const BrepModel& body, ProjectionView& view, const Vector3D& dir) {
for (size_t ei = 0; ei < body.num_edges(); ++ei) {
const auto& e = body.edge(static_cast<int>(ei));
// Get vertex positions
Point3D p0(0,0,0), p1(0,0,0);
bool found0 = false, found1 = false;
for (size_t vi = 0; vi < body.num_vertices(); ++vi) {
const auto& v = body.vertex(static_cast<int>(vi));
if (v.id == e.v_start) { p0 = v.point; found0 = true; }
if (v.id == e.v_end) { p1 = v.point; found1 = true; }
}
if (!found0 || !found1) continue;
auto [x1, y1] = project(p0, dir);
auto [x2, y2] = project(p1, dir);
view.segments.push_back({x1, y1, x2, y2, false});
}
}
} // namespace
std::vector<ProjectionView> generate_views(const BrepModel& body) {
std::vector<ProjectionView> views;
// Front view: looking from -Z
ProjectionView front;
front.name = "Front";
front.view_direction = Vector3D(0, 0, -1);
add_edge_segments(body, front, front.view_direction);
views.push_back(std::move(front));
// Top view: looking from -Y
ProjectionView top;
top.name = "Top";
top.view_direction = Vector3D(0, -1, 0);
add_edge_segments(body, top, top.view_direction);
views.push_back(std::move(top));
// Right view: looking from +X
ProjectionView right;
right.name = "Right";
right.view_direction = Vector3D(1, 0, 0);
add_edge_segments(body, right, right.view_direction);
views.push_back(std::move(right));
return views;
}
ProjectionView section_view(const BrepModel& body,
const Point3D& plane_pt, const Vector3D& plane_n) {
ProjectionView view;
view.name = "Section";
view.view_direction = plane_n;
Vector3D n = plane_n.normalized();
for (size_t ei = 0; ei < body.num_edges(); ++ei) {
const auto& e = body.edge(static_cast<int>(ei));
Point3D p0(0,0,0), p1(0,0,0);
bool f0 = false, f1 = false;
for (size_t vi = 0; vi < body.num_vertices(); ++vi) {
const auto& v = body.vertex(static_cast<int>(vi));
if (v.id == e.v_start) { p0 = v.point; f0 = true; }
if (v.id == e.v_end) { p1 = v.point; f1 = true; }
}
if (!f0 || !f1) continue;
double d0 = (p0 - plane_pt).dot(n);
double d1 = (p1 - plane_pt).dot(n);
// Edge crosses the section plane
if (d0 * d1 < 0 || std::abs(d0) < 1e-9 || std::abs(d1) < 1e-9) {
double t = (std::abs(d0) < 1e-9) ? 0.0 : d0 / (d0 - d1);
t = std::clamp(t, 0.0, 1.0);
Point3D isect = p0 + t * (p1 - p0);
auto [sx, sy] = project(isect, plane_n);
// Store as point (degenerate segment marking intersection location)
view.segments.push_back({sx, sy, sx, sy, false});
}
}
return view;
}
bool export_dxf(const std::string& filepath, const std::vector<ProjectionView>& views) {
std::ofstream f(filepath);
if (!f) return false;
f << "0\nSECTION\n2\nENTITIES\n";
for (const auto& view : views) {
for (const auto& seg : view.segments) {
f << "0\nLINE\n8\n" << view.name << "\n";
f << "10\n" << seg.x1 << "\n20\n" << seg.y1 << "\n";
f << "11\n" << seg.x2 << "\n21\n" << seg.y2 << "\n";
}
}
f << "0\nENDSEC\n0\nEOF\n";
return true;
}
std::vector<Dimension> auto_dimension(const ProjectionView& view) {
if (view.segments.empty()) return {};
// Find bounding box
double xmin = view.segments[0].x1, xmax = view.segments[0].x1;
double ymin = view.segments[0].y1, ymax = view.segments[0].y1;
for (auto& s : view.segments) {
xmin = std::min({xmin, s.x1, s.x2});
xmax = std::max({xmax, s.x1, s.x2});
ymin = std::min({ymin, s.y1, s.y2});
ymax = std::max({ymax, s.y1, s.y2});
}
std::vector<Dimension> dims;
double offset = 10.0;
// Overall width
char buf[64];
snprintf(buf, sizeof(buf), "%.1f", xmax - xmin);
dims.push_back({DimType::Linear, xmin, ymin - offset, xmax, ymin - offset, buf});
// Overall height
snprintf(buf, sizeof(buf), "%.1f", ymax - ymin);
dims.push_back({DimType::Linear, xmin - offset, ymin, xmin - offset, ymax, buf});
return dims;
}
} // namespace vde::brep
+1
View File
@@ -12,3 +12,4 @@ add_vde_test(test_measure)
add_vde_test(test_assembly_constraints) add_vde_test(test_assembly_constraints)
add_vde_test(test_trimmed_surface) add_vde_test(test_trimmed_surface)
add_vde_test(test_brep_heal) add_vde_test(test_brep_heal)
add_vde_test(test_brep_drawing)
+244
View File
@@ -0,0 +1,244 @@
#include <gtest/gtest.h>
#include "vde/brep/brep_drawing.h"
#include "vde/brep/modeling.h"
#include <cmath>
#include <fstream>
#include <cstdio>
using namespace vde::brep;
using namespace vde::core;
// ═══════════════════════════════════════════════════════════
// generate_views — 基本测试
// ═══════════════════════════════════════════════════════════
TEST(BrepDrawingTest, GenerateViews_Box_ReturnsFourViews) {
auto box = make_box(2, 3, 4);
auto views = generate_views(box);
EXPECT_EQ(views.size(), 4u);
EXPECT_EQ(views[0].name, "front");
EXPECT_EQ(views[1].name, "top");
EXPECT_EQ(views[2].name, "right");
EXPECT_EQ(views[3].name, "iso");
}
TEST(BrepDrawingTest, GenerateViews_Box_EachViewHasSegments) {
auto box = make_box(2, 3, 4);
auto views = generate_views(box);
for (const auto& v : views) {
EXPECT_GT(v.total_segments(), 0u)
<< "View " << v.name << " should have segments";
}
}
TEST(BrepDrawingTest, GenerateViews_Box_FrontViewHasExpectedContent) {
auto box = make_box(2, 2, 2);
auto views = generate_views(box);
const auto& front = views[0];
EXPECT_EQ(front.name, "front");
// Front view: XY plane projection, should show X=[-1,1], Y=[-1,1]
EXPECT_GT(front.total_segments(), 0u);
}
TEST(BrepDrawingTest, GenerateViews_Box_ViewDirections) {
auto box = make_box(1, 1, 1);
auto views = generate_views(box);
// Front: looking down -Z
EXPECT_NEAR(views[0].view_direction.z(), -1.0, 1e-6);
// Top: looking down -Y
EXPECT_NEAR(views[1].view_direction.y(), -1.0, 1e-6);
// Right: looking down -X
EXPECT_NEAR(views[2].view_direction.x(), -1.0, 1e-6);
}
TEST(BrepDrawingTest, GenerateViews_EmptyBody_ReturnsEmpty) {
BrepModel empty;
auto views = generate_views(empty);
EXPECT_TRUE(views.empty());
}
TEST(BrepDrawingTest, GenerateViews_Cylinder_ReturnsFourViews) {
auto cyl = make_cylinder(1.0, 3.0, 16);
auto views = generate_views(cyl);
EXPECT_EQ(views.size(), 4u);
for (const auto& v : views) {
EXPECT_GT(v.total_segments(), 0u);
}
}
TEST(BrepDrawingTest, GenerateViews_Sphere_ReturnsFourViews) {
auto sphere = make_sphere(2.0, 16, 8);
auto views = generate_views(sphere);
EXPECT_EQ(views.size(), 4u);
// 球所有视图的主要轮廓应类似
for (const auto& v : views) {
ASSERT_GT(v.total_segments(), 0u);
}
}
// ═══════════════════════════════════════════════════════════
// section_view — 测试
// ═══════════════════════════════════════════════════════════
TEST(BrepDrawingTest, SectionView_Box_AtCenter) {
auto box = make_box(4, 4, 4);
// 截平面 z = 0(XY 平面,法向量沿 Z)
auto view = section_view(box, Point3D(0, 0, 0), Vector3D(0, 0, 1));
EXPECT_EQ(view.name, "section");
// 截面应为矩形轮廓,至少有 4 个交点形成轮廓线段
// + hatch 线,总数应 > 4
ASSERT_GT(view.total_segments(), 4u);
}
TEST(BrepDrawingTest, SectionView_Box_HorizontalPlane) {
auto box = make_box(4, 4, 4);
// z = 1 的水平截面(穿过盒子)
auto view = section_view(box, Point3D(0, 0, 1), Vector3D(0, 0, 1));
ASSERT_GT(view.total_segments(), 4u);
}
TEST(BrepDrawingTest, SectionView_Box_PlaneOutside) {
auto box = make_box(2, 2, 2);
// 截平面在盒子之外(z = 5
auto view = section_view(box, Point3D(0, 0, 5), Vector3D(0, 0, 1));
// 交点不足 3 个 → 无法形成截面
EXPECT_LT(view.total_segments(), 3u);
}
TEST(BrepDrawingTest, SectionView_EmptyBody_ReturnsEmpty) {
BrepModel empty;
auto view = section_view(empty, Point3D(0, 0, 0), Vector3D(0, 0, 1));
EXPECT_EQ(view.name, "section");
EXPECT_EQ(view.total_segments(), 0u);
}
TEST(BrepDrawingTest, SectionView_Cylinder_Midplane) {
auto cyl = make_cylinder(2.0, 6.0, 32);
// 垂直圆柱的 z=0 截面 → 圆形截面
auto view = section_view(cyl, Point3D(0, 0, 0), Vector3D(0, 0, 1));
ASSERT_GT(view.segments.size(), 3u);
}
// ═══════════════════════════════════════════════════════════
// DXF 导出 — 测试
// ═══════════════════════════════════════════════════════════
TEST(BrepDrawingTest, ExportDxf_CreatesFile) {
auto box = make_box(2, 2, 2);
auto views = generate_views(box);
const char* filepath = "test_output.dxf";
std::remove(filepath); // 清理旧文件
bool ok = export_dxf(filepath, views);
EXPECT_TRUE(ok);
// 验证文件存在
std::ifstream in(filepath);
EXPECT_TRUE(in.is_open());
std::string content((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
in.close();
// 验证关键结构
EXPECT_NE(content.find("SECTION"), std::string::npos);
EXPECT_NE(content.find("ENTITIES"), std::string::npos);
EXPECT_NE(content.find("LINE"), std::string::npos);
EXPECT_NE(content.find("ENDSEC"), std::string::npos);
EXPECT_NE(content.find("EOF"), std::string::npos);
// 验证有至少一条 LINE 实体
EXPECT_NE(content.find("LINE"), std::string::npos);
std::remove(filepath);
}
TEST(BrepDrawingTest, ExportDxf_HasLayers) {
auto box = make_box(2, 2, 2);
auto views = generate_views(box);
const char* filepath = "test_layers.dxf";
std::remove(filepath);
export_dxf(filepath, views);
std::ifstream in(filepath);
std::string content((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
in.close();
EXPECT_NE(content.find("visible"), std::string::npos);
EXPECT_NE(content.find("DASHED"), std::string::npos);
std::remove(filepath);
}
TEST(BrepDrawingTest, ExportDxf_EmptyViews_StillWritesSkeleton) {
std::vector<ProjectionView> empty_views;
const char* filepath = "test_empty.dxf";
std::remove(filepath);
bool ok = export_dxf(filepath, empty_views);
EXPECT_TRUE(ok);
std::ifstream in(filepath);
std::string content((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
in.close();
EXPECT_NE(content.find("SECTION"), std::string::npos);
EXPECT_NE(content.find("EOF"), std::string::npos);
std::remove(filepath);
}
TEST(BrepDrawingTest, ExportDxf_InvalidPath_ReturnsFalse) {
auto box = make_box(1, 1, 1);
auto views = generate_views(box);
bool ok = export_dxf("/nonexistent_dir_xyz/test.dxf", views);
EXPECT_FALSE(ok);
}
// ═══════════════════════════════════════════════════════════
// 隐藏线 — 测试
// ═══════════════════════════════════════════════════════════
TEST(BrepDrawingTest, HiddenLines_Box_HasHiddenEdges) {
auto box = make_box(2, 2, 2);
auto views = generate_views(box);
// 每个标准视图均应有隐藏边
for (size_t i = 0; i < std::min(views.size(), 3u); ++i) {
// 前/俯/右视图都应该有隐藏线
EXPECT_GE(views[i].hidden_lines.size() + views[i].segments.size(), 1u);
}
}
TEST(BrepDrawingTest, DrawSegment_DefaultNotHidden) {
DrawSegment seg{0, 0, 1, 1};
EXPECT_FALSE(seg.hidden);
}
TEST(BrepDrawingTest, DrawSegment_Hidden) {
DrawSegment seg{0, 0, 1, 1, true};
EXPECT_TRUE(seg.hidden);
}
TEST(BrepDrawingTest, ProjectionView_TotalSegments) {
ProjectionView v;
v.segments = {{0,0,1,0}, {1,0,1,1}};
v.hidden_lines = {{0,1,0,0, true}};
EXPECT_EQ(v.total_segments(), 3u);
}