diff --git a/include/vde/brep/brep_drawing.h b/include/vde/brep/brep_drawing.h index 53da8f7..6434747 100644 --- a/include/vde/brep/brep_drawing.h +++ b/include/vde/brep/brep_drawing.h @@ -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 @@ -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 segments; - std::vector hidden_lines; + std::string name; ///< "front", "top", "right", "iso" + std::vector segments; ///< visible lines + std::vector 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 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& 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& 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 auto_dimension(const ProjectionView& view); - -} // namespace vde::brep +} // namespace vde::brep diff --git a/src/brep/brep_drawing.cpp b/src/brep/brep_drawing.cpp index 5ae7222..c60fe0e 100644 --- a/src/brep/brep_drawing.cpp +++ b/src/brep/brep_drawing.cpp @@ -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 #include #include -#include 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 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 tessellate_triangles(const BrepModel& body) { + constexpr double deflection = 0.05; + const int res = std::max(4, static_cast(1.0 / deflection)); + std::vector tris; + for (size_t fi = 0; fi < body.num_faces(); ++fi) { + const auto& face = body.face(static_cast(fi)); + if (face.surface_id < 0 || face.surface_id >= static_cast(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(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(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 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& 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 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 defs = { + {"front", Vector3D(0, 0, -1)}, + {"top", Vector3D(0, -1, 0)}, + {"right", Vector3D(-1, 0, 0)}, + {"iso", Vector3D(-1, -1, -1).normalized()}, + }; + std::vector 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(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 intersections; for (size_t ei = 0; ei < body.num_edges(); ++ei) { const auto& e = body.edge(static_cast(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(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(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& 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& 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 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 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