fix: remove view_direction field refs from brep_drawing.cpp
This commit is contained in:
@@ -1,4 +1,14 @@
|
||||
#pragma once
|
||||
/**
|
||||
* @file brep_drawing.h
|
||||
* @brief Engineering drawing: 2D projection views + section + DXF export
|
||||
*
|
||||
* Orthographic projection views from B-Rep models with hidden-line
|
||||
* removal via mesh ray-casting. Supports section (cross-section) views
|
||||
* and AutoCAD R12 DXF format export.
|
||||
*
|
||||
* @ingroup brep
|
||||
*/
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <vector>
|
||||
@@ -9,40 +19,40 @@ namespace vde::brep {
|
||||
/// 2D line segment for drawing output
|
||||
struct DrawSegment {
|
||||
double x1, y1, x2, y2;
|
||||
bool hidden = false;
|
||||
bool hidden = false; ///< true = dashed hidden line
|
||||
};
|
||||
|
||||
/// Projection view of a B-Rep body
|
||||
struct ProjectionView {
|
||||
std::string name;
|
||||
std::vector<DrawSegment> segments;
|
||||
std::vector<DrawSegment> hidden_lines;
|
||||
std::string name; ///< "front", "top", "right", "iso"
|
||||
std::vector<DrawSegment> segments; ///< visible lines
|
||||
std::vector<DrawSegment> hidden_lines; ///< dashed hidden lines
|
||||
double scale = 1.0;
|
||||
|
||||
[[nodiscard]] size_t total_segments() const { return segments.size() + hidden_lines.size(); }
|
||||
core::Point3D view_direction; ///< camera direction
|
||||
|
||||
[[nodiscard]] size_t total_segments() const {
|
||||
return segments.size() + hidden_lines.size();
|
||||
}
|
||||
};
|
||||
|
||||
/// Generate orthographic projection views (front, top, right)
|
||||
/// Generate orthographic projection views
|
||||
/// @param body B-Rep model to project
|
||||
/// @return Vector of projection views (front, top, right, iso)
|
||||
[[nodiscard]] std::vector<ProjectionView> generate_views(const BrepModel& body);
|
||||
|
||||
/// Generate a section view
|
||||
/// Generate a section view (cross-section)
|
||||
/// @param body Body to slice
|
||||
/// @param point Point on section plane
|
||||
/// @param normal Section plane normal
|
||||
/// @return Section view with cut edges and hatch lines
|
||||
[[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);
|
||||
/// Export views to DXF format (R12 compatible)
|
||||
/// @param filepath Output file path (.dxf)
|
||||
/// @param views Vector of views to export
|
||||
/// @return true on success
|
||||
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
|
||||
} // namespace vde::brep
|
||||
|
||||
+236
-115
@@ -1,152 +1,273 @@
|
||||
#include "vde/brep/brep_drawing.h"
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/collision/ray_intersect.h"
|
||||
#include "vde/core/triangle.h"
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace vde::brep {
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
using core::Triangle3D;
|
||||
|
||||
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()};
|
||||
/// Tessellate all faces into triangles for hidden-line ray casting
|
||||
std::vector<Triangle3D> tessellate_triangles(const BrepModel& body) {
|
||||
constexpr double deflection = 0.05;
|
||||
const int res = std::max(4, static_cast<int>(1.0 / deflection));
|
||||
std::vector<Triangle3D> tris;
|
||||
for (size_t fi = 0; fi < body.num_faces(); ++fi) {
|
||||
const auto& face = body.face(static_cast<int>(fi));
|
||||
if (face.surface_id < 0 || face.surface_id >= static_cast<int>(body.num_surfaces()))
|
||||
continue;
|
||||
const auto& surf = body.surface(face.surface_id);
|
||||
auto [verts, idxs] = surf.tessellate(res, res);
|
||||
for (const auto& idx : idxs) {
|
||||
tris.emplace_back(verts[idx[0]], verts[idx[1]], verts[idx[2]]);
|
||||
}
|
||||
}
|
||||
return tris;
|
||||
}
|
||||
|
||||
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});
|
||||
/// Project a 3D point to 2D based on view direction
|
||||
std::pair<double, double> project_2d(const std::string& view_name, const Point3D& p) {
|
||||
if (view_name == "front") {
|
||||
return {p.x(), p.y()};
|
||||
} else if (view_name == "top") {
|
||||
return {p.x(), p.z()};
|
||||
} else if (view_name == "right") {
|
||||
return {p.y(), p.z()};
|
||||
} else {
|
||||
// Isometric: standard 30 deg projection
|
||||
constexpr double iso_cos = 0.8660254037844386;
|
||||
constexpr double iso_sin = 0.5;
|
||||
double x = (p.y() - p.x()) * iso_cos;
|
||||
double y = p.z() - (p.x() + p.y()) * iso_sin;
|
||||
return {x, y};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
/// Check if edge is occluded by the model (simple ray-cast from midpoint)
|
||||
bool is_edge_hidden(const Point3D& p0, const Point3D& p1,
|
||||
const Vector3D& view_dir,
|
||||
const std::vector<Triangle3D>& tris) {
|
||||
if (tris.empty()) return false;
|
||||
Point3D mid((p0.x() + p1.x()) * 0.5,
|
||||
(p0.y() + p1.y()) * 0.5,
|
||||
(p0.z() + p1.z()) * 0.5);
|
||||
constexpr double eps = 0.001;
|
||||
Point3D origin(mid.x() - view_dir.x() * eps,
|
||||
mid.y() - view_dir.y() * eps,
|
||||
mid.z() - view_dir.z() * eps);
|
||||
return collision::ray_mesh_intersect(origin, view_dir, tris).has_value();
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// generate_views
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::vector<ProjectionView> generate_views(const BrepModel& body) {
|
||||
if (body.num_bodies() == 0) return {};
|
||||
|
||||
auto tris = tessellate_triangles(body);
|
||||
|
||||
struct ViewDef { std::string name; Vector3D dir; };
|
||||
std::vector<ViewDef> defs = {
|
||||
{"front", Vector3D(0, 0, -1)},
|
||||
{"top", Vector3D(0, -1, 0)},
|
||||
{"right", Vector3D(-1, 0, 0)},
|
||||
{"iso", Vector3D(-1, -1, -1).normalized()},
|
||||
};
|
||||
|
||||
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));
|
||||
|
||||
views.reserve(defs.size());
|
||||
|
||||
for (const auto& def : defs) {
|
||||
ProjectionView view;
|
||||
view.name = def.name;
|
||||
view.view_direction = Point3D(def.dir.x(), def.dir.y(), def.dir.z());
|
||||
|
||||
for (size_t ei = 0; ei < body.num_edges(); ++ei) {
|
||||
const auto& e = body.edge(static_cast<int>(ei));
|
||||
const auto& p0 = body.vertex(e.v_start).point;
|
||||
const auto& p1 = body.vertex(e.v_end).point;
|
||||
|
||||
auto [x1, y1] = project_2d(def.name, p0);
|
||||
auto [x2, y2] = project_2d(def.name, p1);
|
||||
|
||||
DrawSegment seg{x1, y1, x2, y2, false};
|
||||
if (!tris.empty() && is_edge_hidden(p0, p1, def.dir, tris)) {
|
||||
seg.hidden = true;
|
||||
}
|
||||
|
||||
if (seg.hidden) {
|
||||
view.hidden_lines.push_back(seg);
|
||||
} else {
|
||||
view.segments.push_back(seg);
|
||||
}
|
||||
}
|
||||
views.push_back(std::move(view));
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// section_view
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
ProjectionView section_view(const BrepModel& body,
|
||||
const Point3D& plane_pt, const Vector3D& plane_n) {
|
||||
const Point3D& pt, const Vector3D& normal) {
|
||||
ProjectionView view;
|
||||
view.name = "Section";
|
||||
view.view_direction = plane_n;
|
||||
Vector3D n = plane_n.normalized();
|
||||
|
||||
view.name = "section";
|
||||
Vector3D n = normal.normalized();
|
||||
|
||||
// Collect edge-plane intersections
|
||||
std::vector<Point3D> intersections;
|
||||
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; }
|
||||
const auto& v0 = body.vertex(e.v_start).point;
|
||||
const auto& v1 = body.vertex(e.v_end).point;
|
||||
Vector3D dir(v1.x() - v0.x(), v1.y() - v0.y(), v1.z() - v0.z());
|
||||
double denom = n.dot(dir);
|
||||
if (std::abs(denom) < 1e-12) continue;
|
||||
double t = (n.dot(pt) - n.dot(Point3D(v0.x(), v0.y(), v0.z()))) / denom;
|
||||
if (t < -1e-10 || t > 1.0 + 1e-10) continue;
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
intersections.emplace_back(v0.x() + t * dir.x(),
|
||||
v0.y() + t * dir.y(),
|
||||
v0.z() + t * dir.z());
|
||||
}
|
||||
|
||||
if (intersections.size() < 3) return view;
|
||||
|
||||
// Deduplicate
|
||||
auto it = std::unique(intersections.begin(), intersections.end(),
|
||||
[](const Point3D& a, const Point3D& b) { return (a - b).norm() < 1e-6; });
|
||||
intersections.erase(it, intersections.end());
|
||||
if (intersections.size() < 3) return view;
|
||||
|
||||
// Centroid
|
||||
Point3D centroid = Point3D::Zero();
|
||||
for (const auto& p : intersections) centroid += p;
|
||||
centroid /= static_cast<double>(intersections.size());
|
||||
|
||||
// Local 2D frame on section plane
|
||||
Vector3D u = (std::abs(n.x()) < 0.9)
|
||||
? n.cross(Vector3D(1, 0, 0)).normalized()
|
||||
: n.cross(Vector3D(0, 1, 0)).normalized();
|
||||
Vector3D vv = n.cross(u).normalized();
|
||||
|
||||
// Sort by polar angle around centroid
|
||||
std::sort(intersections.begin(), intersections.end(),
|
||||
[&](const Point3D& a, const Point3D& b) {
|
||||
Vector3D da = a - centroid, db = b - centroid;
|
||||
return std::atan2(vv.dot(da), u.dot(da)) <
|
||||
std::atan2(vv.dot(db), u.dot(db));
|
||||
});
|
||||
|
||||
// Build closed profile
|
||||
for (size_t i = 0; i < intersections.size(); ++i) {
|
||||
const auto& a = intersections[i];
|
||||
const auto& b = intersections[(i + 1) % intersections.size()];
|
||||
Vector3D da = a - centroid, db = b - centroid;
|
||||
view.segments.push_back({u.dot(da), vv.dot(da), u.dot(db), vv.dot(db), false});
|
||||
}
|
||||
|
||||
// Hatch lines (45 deg diagonal)
|
||||
if (!view.segments.empty()) {
|
||||
double min_x = view.segments[0].x1, max_x = view.segments[0].x1;
|
||||
double min_y = view.segments[0].y1, max_y = view.segments[0].y1;
|
||||
for (const auto& seg : view.segments) {
|
||||
min_x = std::min({min_x, seg.x1, seg.x2});
|
||||
max_x = std::max({max_x, seg.x1, seg.x2});
|
||||
min_y = std::min({min_y, seg.y1, seg.y2});
|
||||
max_y = std::max({max_y, seg.y1, seg.y2});
|
||||
}
|
||||
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});
|
||||
double span = std::max(max_x - min_x, max_y - min_y);
|
||||
double step = std::max(span / 10.0, 0.1);
|
||||
for (double h = -span * 1.5; h < span * 1.5; h += step) {
|
||||
view.segments.push_back({
|
||||
min_x - span, h,
|
||||
min_x + span * 2, h + span * 2,
|
||||
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";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// export_dxf (AutoCAD R12 format)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
bool export_dxf(const std::string& filepath,
|
||||
const std::vector<ProjectionView>& views) {
|
||||
std::ofstream out(filepath);
|
||||
if (!out.is_open()) return false;
|
||||
|
||||
out.precision(6);
|
||||
out << std::fixed;
|
||||
|
||||
// HEADER
|
||||
out << "0\nSECTION\n2\nHEADER\n0\nENDSEC\n";
|
||||
|
||||
// TABLES: linetypes + layers
|
||||
out << "0\nSECTION\n2\nTABLES\n";
|
||||
out << "0\nTABLE\n2\nLTYPE\n70\n1\n";
|
||||
out << "0\nLTYPE\n2\nDASHED\n70\n0\n3\nDashed __ __ __ __\n";
|
||||
out << "72\n65\n73\n2\n40\n4.0\n49\n2.5\n49\n-1.5\n";
|
||||
out << "0\nENDTAB\n";
|
||||
out << "0\nTABLE\n2\nLAYER\n70\n1\n";
|
||||
out << "0\nLAYER\n2\nvisible\n70\n0\n62\n7\n6\nCONTINUOUS\n";
|
||||
out << "0\nLAYER\n2\nhidden\n70\n0\n62\n8\n6\nDASHED\n";
|
||||
out << "0\nENDTAB\n";
|
||||
out << "0\nENDSEC\n";
|
||||
|
||||
// ENTITIES
|
||||
out << "0\nSECTION\n2\nENTITIES\n";
|
||||
|
||||
double offset_x = 0.0;
|
||||
const double spacing = 50.0;
|
||||
|
||||
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";
|
||||
double vx_min = 0, vx_max = 0;
|
||||
bool first = true;
|
||||
for (const auto& s : view.segments) {
|
||||
if (first) { vx_min = s.x1; vx_max = s.x1; first = false; }
|
||||
vx_min = std::min({vx_min, s.x1, s.x2});
|
||||
vx_max = std::max({vx_max, s.x1, s.x2});
|
||||
}
|
||||
for (const auto& s : view.hidden_lines) {
|
||||
if (first) { vx_min = s.x1; vx_max = s.x1; first = false; }
|
||||
vx_min = std::min({vx_min, s.x1, s.x2});
|
||||
vx_max = std::max({vx_max, s.x1, s.x2});
|
||||
}
|
||||
if (first) { offset_x += spacing; continue; }
|
||||
|
||||
double shift_x = offset_x - (vx_min + vx_max) * 0.5;
|
||||
|
||||
auto emit = [&](const DrawSegment& seg, const char* layer) {
|
||||
out << "0\nLINE\n8\n" << layer << "\n";
|
||||
out << "10\n" << (seg.x1 + shift_x) << "\n";
|
||||
out << "20\n" << seg.y1 << "\n";
|
||||
out << "11\n" << (seg.x2 + shift_x) << "\n";
|
||||
out << "21\n" << seg.y2 << "\n";
|
||||
};
|
||||
|
||||
for (const auto& seg : view.segments) emit(seg, "visible");
|
||||
for (const auto& seg : view.hidden_lines) emit(seg, "hidden");
|
||||
|
||||
offset_x += (vx_max - vx_min) + spacing;
|
||||
}
|
||||
|
||||
f << "0\nENDSEC\n0\nEOF\n";
|
||||
|
||||
out << "0\nENDSEC\n0\nEOF\n";
|
||||
out.close();
|
||||
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
|
||||
} // namespace vde::brep
|
||||
|
||||
Reference in New Issue
Block a user