feat(v4.0): GD&T geometric tolerancing — ASME Y14.5
CI / Build & Test (push) Failing after 35s
CI / Release Build (push) Failing after 35s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

- 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:
茂之钳
2026-07-25 03:25:06 +00:00
parent a2b34733bf
commit 6a98e8cd9c
5 changed files with 794 additions and 0 deletions
+1
View File
@@ -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
+324
View File
@@ -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 << "";
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