#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 namespace vde::brep { using core::Point3D; using core::Vector3D; using core::Triangle3D; namespace { /// 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; } /// 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}; } } /// 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; 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& pt, const Vector3D& normal) { ProjectionView view; 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)); 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}); } 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; } // ═══════════════════════════════════════════════════════════ // 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) { 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; } out << "0\nENDSEC\n0\nEOF\n"; out.close(); return true; } // ── offset_section_view ───────────────────────────────────── ProjectionView offset_section_view(const BrepModel& body, const Vector3D& normal, const std::vector& offsets) { ProjectionView combined; combined.name = "offset_section"; if (offsets.empty()) return combined; Vector3D n = normal.normalized(); // Collect section profiles from each offset plane struct SectionProfile { double offset; std::vector points; // 3D intersection points Vector3D u, vv; // local 2D frame core::Point3D centroid; }; std::vector profiles; for (double d : offsets) { core::Point3D pt = core::Point3D(n.x() * d, n.y() * d, n.z() * d); auto view = section_view(body, pt, normal); if (view.segments.empty()) continue; SectionProfile prof; prof.offset = d; // Reconstruct 3D points from 2D segments for boundary tracking // Use the first segment's endpoints as representative points for (const auto& seg : view.segments) { // Convert 2D → 3D using the section plane frame // (simplified: just store the profile for now) break; // just need the profile definition } profiles.push_back(prof); } if (profiles.empty()) return combined; // For simplicity: merge by generating combined section at the middle plane // Each offset plane produces a section profile. We project all profiles // onto a common 2D plane and concatenate them with step lines. double mid_off = (offsets.front() + offsets.back()) * 0.5; core::Point3D mid_pt = core::Point3D(n.x() * mid_off, n.y() * mid_off, n.z() * mid_off); combined = section_view(body, mid_pt, normal); combined.name = "offset_section"; // For stepped sections: add boundary lines between adjacent offset regions. // Each step is a vertical line connecting the section boundary at one offset // to the next. We generate these by sectioning at each offset. for (size_t i = 0; i < offsets.size(); ++i) { core::Point3D pt_i(n.x() * offsets[i], n.y() * offsets[i], n.z() * offsets[i]); auto sec_i = section_view(body, pt_i, normal); // Project each section onto a common plane perpendicular to the viewing direction // For viewing along the section normal (XZ plane): Y maps to depth for (const auto& seg : sec_i.segments) { if (seg.hidden) continue; // Add offset step lines (parallel to section normal) if (i < offsets.size() - 1) { double next_d = offsets[i + 1]; double depth = next_d - offsets[i]; combined.segments.push_back({ seg.x1, offsets[i], seg.x1, next_d, false }); combined.segments.push_back({ seg.x2, offsets[i], seg.x2, next_d, false }); } } } return combined; } } // namespace vde::brep