feat(v4.0): GD&T geometric tolerancing — ASME Y14.5
- 14 tolerance types: flatness, parallelism, perpendicularity, concentricity, position, etc. - GDTFeature control frame with symbols, modifiers (MMC/LMC/P), datum refs - Validation: flatness/parallelism/perpendicularity/concentricity/position - DXF export for GD&T annotations - 22 tests: all symbols, modifiers, validation, DXF export
This commit is contained in:
@@ -149,6 +149,7 @@ add_library(vde_brep STATIC
|
||||
brep/interference_check.cpp
|
||||
brep/motion_simulation.cpp
|
||||
brep/explode_view.cpp
|
||||
brep/gdt.cpp
|
||||
brep/modeling.cpp
|
||||
brep/brep_drawing.cpp
|
||||
brep/step_export.cpp
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
#include "vde/brep/gdt.h"
|
||||
#include "vde/core/aabb.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <map>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
using core::AABB3D;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Symbol lookup
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
static const std::map<GDTType, std::string> kSymbols = {
|
||||
{GDTType::Flatness, "⏥"},
|
||||
{GDTType::Straightness, "—"},
|
||||
{GDTType::Circularity, "○"},
|
||||
{GDTType::Cylindricity, "⌭"},
|
||||
{GDTType::Parallelism, "∥"},
|
||||
{GDTType::Perpendicularity, "⟂"},
|
||||
{GDTType::Angularity, "∠"},
|
||||
{GDTType::Position, "⌖"},
|
||||
{GDTType::Concentricity, "◎"},
|
||||
{GDTType::Symmetry, "⌯"},
|
||||
{GDTType::ProfileOfLine, "⌒"},
|
||||
{GDTType::ProfileOfSurface, "⌓"},
|
||||
{GDTType::CircularRunout, "↗"},
|
||||
{GDTType::TotalRunout, "↗↗"},
|
||||
};
|
||||
|
||||
static const std::map<GDTType, std::string> kNames = {
|
||||
{GDTType::Flatness, "FLATNESS"},
|
||||
{GDTType::Straightness, "STRAIGHTNESS"},
|
||||
{GDTType::Circularity, "CIRCULARITY"},
|
||||
{GDTType::Cylindricity, "CYLINDRICITY"},
|
||||
{GDTType::Parallelism, "PARALLELISM"},
|
||||
{GDTType::Perpendicularity, "PERPENDICULARITY"},
|
||||
{GDTType::Angularity, "ANGULARITY"},
|
||||
{GDTType::Position, "POSITION"},
|
||||
{GDTType::Concentricity, "CONCENTRICITY"},
|
||||
{GDTType::Symmetry, "SYMMETRY"},
|
||||
{GDTType::ProfileOfLine, "PROFILE_OF_LINE"},
|
||||
{GDTType::ProfileOfSurface, "PROFILE_OF_SURFACE"},
|
||||
{GDTType::CircularRunout, "CIRCULAR_RUNOUT"},
|
||||
{GDTType::TotalRunout, "TOTAL_RUNOUT"},
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GDTFeature methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::string GDTFeature::symbol() const {
|
||||
auto it = kSymbols.find(type);
|
||||
return it != kSymbols.end() ? it->second : "?";
|
||||
}
|
||||
|
||||
std::string GDTFeature::to_string() const {
|
||||
std::ostringstream ss;
|
||||
ss << symbol() << " ";
|
||||
|
||||
if (zone_shape == ZoneShape::Cylindrical) ss << "Ø";
|
||||
else if (zone_shape == ZoneShape::Spherical) ss << "SØ";
|
||||
|
||||
ss << tolerance_value;
|
||||
|
||||
if (mmc) ss << " M";
|
||||
if (lmc) ss << " L";
|
||||
if (projected) ss << " P" << projected_height;
|
||||
|
||||
for (const auto& d : datums) {
|
||||
ss << " " << d.label;
|
||||
if (d.is_primary) ss << " (primary)";
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string GDTAnnotation::to_dxf_text() const {
|
||||
std::ostringstream ss;
|
||||
ss << feature.to_string()
|
||||
<< " @(" << frame_position.x() << "," << frame_position.y() << ")";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Geometric computations
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
/// Sample points from a face by tessellating to mesh and extracting vertices
|
||||
std::vector<Point3D> sample_face_points(const BrepModel& body, int face_id, int num_samples = 50) {
|
||||
if (face_id < 0 || face_id >= static_cast<int>(body.num_faces())) return {};
|
||||
|
||||
auto mesh = body.to_mesh(0.1);
|
||||
std::vector<Point3D> points;
|
||||
|
||||
// Get edges of the target face
|
||||
auto face_edges_ids = body.face_edges(face_id);
|
||||
|
||||
// Collect vertices referenced by the face's edges
|
||||
for (int ei : face_edges_ids) {
|
||||
const auto& e = body.edge(ei);
|
||||
try {
|
||||
points.push_back(body.vertex_by_id(e.v_start).point);
|
||||
points.push_back(body.vertex_by_id(e.v_end).point);
|
||||
} catch (...) {
|
||||
// vertex not found, skip
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate
|
||||
std::sort(points.begin(), points.end(),
|
||||
[](const Point3D& a, const Point3D& b) {
|
||||
if (a.x() != b.x()) return a.x() < b.x();
|
||||
if (a.y() != b.y()) return a.y() < b.y();
|
||||
return a.z() < b.z();
|
||||
});
|
||||
points.erase(std::unique(points.begin(), points.end(),
|
||||
[](const Point3D& a, const Point3D& b) {
|
||||
return (a - b).norm() < 1e-9;
|
||||
}), points.end());
|
||||
|
||||
// If not enough points from vertices, sample mesh
|
||||
if (points.size() < 4) {
|
||||
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
|
||||
auto fv = mesh.face_vertices(static_cast<int>(fi));
|
||||
points.push_back(mesh.vertex(fv[0]));
|
||||
points.push_back(mesh.vertex(fv[1]));
|
||||
points.push_back(mesh.vertex(fv[2]));
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/// Fit a plane to points using cross-product of two edges (simplified PCA)
|
||||
/// Returns (center, normal)
|
||||
std::pair<Point3D, Vector3D> fit_plane(const std::vector<Point3D>& pts) {
|
||||
if (pts.size() < 3) return {Point3D(0,0,0), Vector3D(0,0,1)};
|
||||
|
||||
Point3D center(0, 0, 0);
|
||||
for (const auto& p : pts) center = center + p;
|
||||
center = center * (1.0 / pts.size());
|
||||
|
||||
// Compute normal via cross product of first two non-collinear edges
|
||||
Vector3D n(0, 0, 1);
|
||||
for (size_t i = 0; i + 2 < pts.size(); ++i) {
|
||||
Vector3D e1 = pts[i + 1] - pts[i];
|
||||
Vector3D e2 = pts[i + 2] - pts[i];
|
||||
n = e1.cross(e2);
|
||||
if (n.norm() > 1e-12) { n.normalize(); break; }
|
||||
}
|
||||
|
||||
return {center, n};
|
||||
}
|
||||
|
||||
Vector3D face_center(const BrepModel& body, int face_id) {
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.empty()) return Vector3D(0, 0, 0);
|
||||
Vector3D c(0, 0, 0);
|
||||
for (const auto& p : pts) c = c + Vector3D(p.x(), p.y(), p.z());
|
||||
return c * (1.0 / pts.size());
|
||||
}
|
||||
|
||||
Vector3D face_normal(const BrepModel& body, int face_id) {
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
return fit_plane(pts).second;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Public GD&T functions
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
double compute_flatness(const BrepModel& body, int face_id) {
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.size() < 3) return 0.0;
|
||||
|
||||
auto [center, normal] = fit_plane(pts);
|
||||
|
||||
double max_dev = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
double d = std::abs((p - center).dot(normal));
|
||||
if (d > max_dev) max_dev = d;
|
||||
}
|
||||
return max_dev;
|
||||
}
|
||||
|
||||
double compute_parallelism(const BrepModel& body, int face_id, int datum_face_id) {
|
||||
Vector3D datum_normal = face_normal(body, datum_face_id);
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.size() < 3) return 0.0;
|
||||
|
||||
// Project face points onto datum normal direction
|
||||
double min_proj = std::numeric_limits<double>::max();
|
||||
double max_proj = -std::numeric_limits<double>::max();
|
||||
for (const auto& p : pts) {
|
||||
double proj = p.dot(datum_normal);
|
||||
if (proj < min_proj) min_proj = proj;
|
||||
if (proj > max_proj) max_proj = proj;
|
||||
}
|
||||
return max_proj - min_proj;
|
||||
}
|
||||
|
||||
double compute_perpendicularity(const BrepModel& body, int face_id, int datum_face_id) {
|
||||
Vector3D datum_normal = face_normal(body, datum_face_id);
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.size() < 3) return 0.0;
|
||||
|
||||
// The face should be perpendicular to datum → face normal ⟂ datum normal
|
||||
Vector3D face_n = fit_plane(pts).second;
|
||||
|
||||
// Deviation from perfect perpendicularity = |face_n · datum_n|
|
||||
double dot = std::abs(face_n.dot(datum_normal));
|
||||
|
||||
// Scale by face extent as rough deviation
|
||||
AABB3D bb;
|
||||
for (const auto& p : pts) bb.expand(p);
|
||||
double extent = bb.extent().norm();
|
||||
|
||||
return dot * extent;
|
||||
}
|
||||
|
||||
double compute_concentricity(const BrepModel& body, int face_id, int datum_face_id) {
|
||||
Vector3D c1 = face_center(body, face_id);
|
||||
Vector3D c2 = face_center(body, datum_face_id);
|
||||
|
||||
// Project onto plane perpendicular to primary axis
|
||||
Vector3D offset(c1.x() - c2.x(), c1.y() - c2.y(), 0);
|
||||
return offset.norm();
|
||||
}
|
||||
|
||||
double compute_position(const BrepModel& body, int face_id,
|
||||
const Point3D& theoretical_position) {
|
||||
Vector3D actual = face_center(body, face_id);
|
||||
return (Vector3D(actual.x(), actual.y(), actual.z()) -
|
||||
Vector3D(theoretical_position.x(), theoretical_position.y(), theoretical_position.z())).norm();
|
||||
}
|
||||
|
||||
GDTValidationResult validate_gdt(const BrepModel& body, const GDTFeature& feature) {
|
||||
GDTValidationResult result;
|
||||
result.tolerance = feature.tolerance_value;
|
||||
|
||||
switch (feature.type) {
|
||||
case GDTType::Flatness:
|
||||
result.actual_deviation = compute_flatness(body, feature.target_face_id);
|
||||
break;
|
||||
case GDTType::Parallelism:
|
||||
if (!feature.datums.empty()) {
|
||||
result.actual_deviation = compute_parallelism(
|
||||
body, feature.target_face_id, feature.datums[0].face_id);
|
||||
}
|
||||
break;
|
||||
case GDTType::Perpendicularity:
|
||||
if (!feature.datums.empty()) {
|
||||
result.actual_deviation = compute_perpendicularity(
|
||||
body, feature.target_face_id, feature.datums[0].face_id);
|
||||
}
|
||||
break;
|
||||
case GDTType::Concentricity:
|
||||
if (!feature.datums.empty()) {
|
||||
result.actual_deviation = compute_concentricity(
|
||||
body, feature.target_face_id, feature.datums[0].face_id);
|
||||
}
|
||||
break;
|
||||
case GDTType::Position:
|
||||
result.actual_deviation = compute_position(
|
||||
body, feature.target_face_id, Point3D(0, 0, 0));
|
||||
break;
|
||||
default:
|
||||
result.warnings.push_back("GD&T type not yet implemented for validation");
|
||||
result.actual_deviation = 0.0;
|
||||
}
|
||||
|
||||
result.pass = result.actual_deviation <= feature.tolerance_value;
|
||||
|
||||
auto name_it = kNames.find(feature.type);
|
||||
std::string type_name = name_it != kNames.end() ? name_it->second : "UNKNOWN";
|
||||
|
||||
std::ostringstream desc;
|
||||
desc << type_name << ": actual=" << result.actual_deviation
|
||||
<< " tolerance=" << feature.tolerance_value
|
||||
<< " → " << (result.pass ? "PASS" : "FAIL");
|
||||
result.description = desc.str();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string export_gdt_annotations_dxf(
|
||||
const std::vector<GDTAnnotation>& annotations) {
|
||||
std::ostringstream ss;
|
||||
ss << "0\nSECTION\n2\nENTITIES\n";
|
||||
|
||||
for (const auto& ann : annotations) {
|
||||
// MTEXT entity for GD&T frame
|
||||
ss << "0\nMTEXT\n";
|
||||
ss << "8\nGDT\n"; // layer
|
||||
ss << "10\n" << ann.frame_position.x() << "\n";
|
||||
ss << "20\n" << ann.frame_position.y() << "\n";
|
||||
ss << "30\n0.0\n";
|
||||
ss << "1\n" << ann.feature.to_string() << "\n";
|
||||
|
||||
// LEADER line
|
||||
ss << "0\nLINE\n";
|
||||
ss << "8\nGDT\n";
|
||||
ss << "10\n" << ann.frame_position.x() << "\n";
|
||||
ss << "20\n" << ann.frame_position.y() << "\n";
|
||||
ss << "30\n0.0\n";
|
||||
ss << "11\n" << ann.leader_point.x() << "\n";
|
||||
ss << "21\n" << ann.leader_point.y() << "\n";
|
||||
ss << "31\n0.0\n";
|
||||
}
|
||||
|
||||
ss << "0\nENDSEC\n0\nEOF\n";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
Reference in New Issue
Block a user