feat(v3.8): 2D projection views + section + DXF + dimensions + G2 curvature
This commit is contained in:
@@ -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
|
||||
@@ -145,6 +145,7 @@ add_library(vde::engine ALIAS vde)
|
||||
add_library(vde_brep STATIC
|
||||
brep/brep.cpp
|
||||
brep/modeling.cpp
|
||||
brep/brep_drawing.cpp
|
||||
brep/step_export.cpp
|
||||
brep/step_import.cpp
|
||||
brep/iges_import.cpp
|
||||
|
||||
@@ -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
|
||||
@@ -12,3 +12,4 @@ add_vde_test(test_measure)
|
||||
add_vde_test(test_assembly_constraints)
|
||||
add_vde_test(test_trimmed_surface)
|
||||
add_vde_test(test_brep_heal)
|
||||
add_vde_test(test_brep_drawing)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user