feat(v6.0): FFD freeform deformation + auto-dimensioning + PMI/MBD + Web viewer
v6.0.1 — FFD Freeform Deformation: - ffd_deformation.h/.cpp: Bezier volume lattice, deform_brep/deform_mesh - Bernstein polynomial mapping, topology-preserving - Cage-based + lattice-based FFD, create_cage_lattice() v6.0.2 — Auto-Dimensioning + PMI/MBD + GD&T: - auto_dimensioning.h/.cpp: linear/angular/radial/diameter detection - ISO/ANSI/JIS drawing standards, smart label placement - gdt.h/.cpp: 14 GD&T symbols (ASME Y14.5), MMC/LMC/RFS material conditions - pmi_mbd.h/.cpp: PMIAnnotation, attach_pmi, PMIView, STEP AP242 export - 70/70 tests passing (23 auto-dim + 25 PMI + 22 GD&T regression) v6.0.3 — Web 3D Viewer Enhancement: - viewer/index.html + app.js: Three.js GLB/STL loading - OrbitControls, face coloring, dimension overlay, GD&T display - Measure tool (raycaster), clip plane slider, explode animation - Drag-and-drop file upload, responsive layout
This commit is contained in:
@@ -166,6 +166,8 @@ add_library(vde_brep STATIC
|
||||
brep/tolerance.cpp
|
||||
brep/modeling.cpp
|
||||
brep/brep_drawing.cpp
|
||||
brep/auto_dimensioning.cpp
|
||||
brep/pmi_mbd.cpp
|
||||
brep/step_export.cpp
|
||||
brep/step_import.cpp
|
||||
brep/iges_import.cpp
|
||||
@@ -194,6 +196,7 @@ add_library(vde_brep STATIC
|
||||
brep/fastener_library.cpp
|
||||
brep/flexible_assembly.cpp
|
||||
brep/assembly_lod.cpp
|
||||
brep/ffd_deformation.cpp
|
||||
)
|
||||
target_include_directories(vde_brep
|
||||
PUBLIC ${CMAKE_SOURCE_DIR}/include
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
#include "vde/brep/auto_dimensioning.h"
|
||||
#include "vde/core/aabb.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <limits>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
using core::AABB3D;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Internal helpers
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
/// Project a 3D point to 2D based on view direction
|
||||
std::pair<double, double> project_to_view(const Point3D& p, const Vector3D& dir) {
|
||||
// Build a local 2D frame perpendicular to the view direction
|
||||
Vector3D ndir = dir.normalized();
|
||||
Vector3D u = (std::abs(ndir.x()) < 0.9)
|
||||
? ndir.cross(Vector3D(1, 0, 0)).normalized()
|
||||
: ndir.cross(Vector3D(0, 1, 0)).normalized();
|
||||
Vector3D v = ndir.cross(u).normalized();
|
||||
Vector3D d(p.x(), p.y(), p.z());
|
||||
return {u.dot(d), v.dot(d)};
|
||||
}
|
||||
|
||||
/// Check if two edges are parallel (dot product of directions ≈ ±1)
|
||||
bool edges_parallel(const BrepModel& body, int e0, int e1) {
|
||||
const auto& v0_start = body.vertex(body.edge(e0).v_start).point;
|
||||
const auto& v0_end = body.vertex(body.edge(e0).v_end).point;
|
||||
const auto& v1_start = body.vertex(body.edge(e1).v_start).point;
|
||||
const auto& v1_end = body.vertex(body.edge(e1).v_end).point;
|
||||
|
||||
Vector3D d0(v0_end.x() - v0_start.x(), v0_end.y() - v0_start.y(), v0_end.z() - v0_start.z());
|
||||
Vector3D d1(v1_end.x() - v1_start.x(), v1_end.y() - v1_start.y(), v1_end.z() - v1_start.z());
|
||||
|
||||
double len0 = d0.norm();
|
||||
double len1 = d1.norm();
|
||||
if (len0 < 1e-12 || len1 < 1e-12) return false;
|
||||
|
||||
double cos_angle = std::abs(d0.dot(d1)) / (len0 * len1);
|
||||
return cos_angle > 0.999; // ~2.5° tolerance
|
||||
}
|
||||
|
||||
/// Calculate distance between two parallel edges (distance between lines)
|
||||
double parallel_edge_distance(const BrepModel& body, int e0, int e1) {
|
||||
const auto& p0 = body.vertex(body.edge(e0).v_start).point;
|
||||
const auto& q0 = body.vertex(body.edge(e1).v_start).point;
|
||||
|
||||
Vector3D dir0(body.vertex(body.edge(e0).v_end).point.x() - p0.x(),
|
||||
body.vertex(body.edge(e0).v_end).point.y() - p0.y(),
|
||||
body.vertex(body.edge(e0).v_end).point.z() - p0.z());
|
||||
dir0.normalize();
|
||||
|
||||
Vector3D diff(q0.x() - p0.x(), q0.y() - p0.y(), q0.z() - p0.z());
|
||||
Vector3D perp = diff - dir0 * dir0.dot(diff);
|
||||
return perp.norm();
|
||||
}
|
||||
|
||||
/// Calculate angle between two edges (in degrees)
|
||||
double edge_angle(const BrepModel& body, int e0, int e1) {
|
||||
const auto& v0_start = body.vertex(body.edge(e0).v_start).point;
|
||||
const auto& v0_end = body.vertex(body.edge(e0).v_end).point;
|
||||
const auto& v1_start = body.vertex(body.edge(e1).v_start).point;
|
||||
const auto& v1_end = body.vertex(body.edge(e1).v_end).point;
|
||||
|
||||
Vector3D d0(v0_end.x() - v0_start.x(), v0_end.y() - v0_start.y(), v0_end.z() - v0_start.z());
|
||||
Vector3D d1(v1_end.x() - v1_start.x(), v1_end.y() - v1_start.y(), v1_end.z() - v1_start.z());
|
||||
|
||||
double len0 = d0.norm();
|
||||
double len1 = d1.norm();
|
||||
if (len0 < 1e-12 || len1 < 1e-12) return 0.0;
|
||||
|
||||
double cos_a = d0.dot(d1) / (len0 * len1);
|
||||
cos_a = std::max(-1.0, std::min(1.0, cos_a));
|
||||
return std::acos(cos_a) * 180.0 / M_PI;
|
||||
}
|
||||
|
||||
/// Detect if an edge is an arc (has a curve that is a circle/arc)
|
||||
bool is_arc_edge(const BrepModel& body, int ei) {
|
||||
const auto& e = body.edge(ei);
|
||||
return e.curve != nullptr && e.curve->degree() == 2 &&
|
||||
e.curve->control_points().size() >= 5;
|
||||
}
|
||||
|
||||
/// Detect if an edge forms a closed circle (start ≈ end)
|
||||
bool is_closed_circle(const BrepModel& body, int ei) {
|
||||
const auto& e = body.edge(ei);
|
||||
if (!e.curve) return false;
|
||||
if (e.curve->degree() != 2) return false;
|
||||
auto& cp = e.curve->control_points();
|
||||
if (cp.size() < 5) return false;
|
||||
double dist = (cp.front() - cp.back()).norm();
|
||||
return dist < 1e-6;
|
||||
}
|
||||
|
||||
/// Compute radius from arc/circle edge (midpoint of arc to center)
|
||||
double estimate_radius(const BrepModel& body, int ei) {
|
||||
const auto& e = body.edge(ei);
|
||||
if (!e.curve || e.curve->control_points().size() < 5) return 0.0;
|
||||
|
||||
auto& cp = e.curve->control_points();
|
||||
Point3D start = cp[0];
|
||||
Point3D mid = cp[cp.size() / 2];
|
||||
Point3D end = cp.back();
|
||||
|
||||
// For a degree-2 circle: radius = chord / (2*sin(angle/2))
|
||||
// Simplified: use average distance from center of control polygon
|
||||
Point3D center(0, 0, 0);
|
||||
for (const auto& p : cp) center = center + p;
|
||||
center = center * (1.0 / cp.size());
|
||||
|
||||
double sum_r = 0.0;
|
||||
for (const auto& p : cp) {
|
||||
sum_r += (p - center).norm();
|
||||
}
|
||||
return sum_r / cp.size();
|
||||
}
|
||||
|
||||
/// Compute edge midpoint in 3D
|
||||
Point3D edge_midpoint(const BrepModel& body, int ei) {
|
||||
const auto& e = body.edge(ei);
|
||||
const auto& v0 = body.vertex(e.v_start).point;
|
||||
const auto& v1 = body.vertex(e.v_end).point;
|
||||
return Point3D((v0.x() + v1.x()) * 0.5,
|
||||
(v0.y() + v1.y()) * 0.5,
|
||||
(v0.z() + v1.z()) * 0.5);
|
||||
}
|
||||
|
||||
/// Given a set of pairs (dim value, position), place them avoiding overlap
|
||||
void avoid_overlap_impl(std::vector<std::pair<double, double>>& placement_ys,
|
||||
double spacing, double start_y) {
|
||||
if (placement_ys.empty()) return;
|
||||
|
||||
std::sort(placement_ys.begin(), placement_ys.end(),
|
||||
[](const auto& a, const auto& b) { return a.second < b.second; });
|
||||
|
||||
double last_y = placement_ys[0].second;
|
||||
for (size_t i = 1; i < placement_ys.size(); ++i) {
|
||||
if (placement_ys[i].second < last_y + spacing) {
|
||||
placement_ys[i].second = last_y + spacing;
|
||||
}
|
||||
last_y = placement_ys[i].second;
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Dimension methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::string Dimension::to_text() const {
|
||||
std::ostringstream ss;
|
||||
ss << prefix;
|
||||
if (!text_override.empty()) {
|
||||
ss << text_override;
|
||||
} else {
|
||||
ss << std::fixed << std::setprecision(2) << value;
|
||||
}
|
||||
if (has_tolerance()) {
|
||||
ss << std::showpos << std::fixed << std::setprecision(3)
|
||||
<< tolerance_upper;
|
||||
if (tolerance_lower != 0.0) {
|
||||
ss << "/" << tolerance_lower;
|
||||
}
|
||||
ss << std::noshowpos;
|
||||
}
|
||||
ss << suffix;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string Dimension::to_dxf_text() const {
|
||||
std::ostringstream ss;
|
||||
ss << to_text()
|
||||
<< " @(" << text_position.x() << "," << text_position.y() << ")";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// StandardConfig
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::string StandardConfig::standard_name() const {
|
||||
switch (standard) {
|
||||
case DrawingStandard::ISO: return "ISO 129";
|
||||
case DrawingStandard::ANSI: return "ANSI Y14.5";
|
||||
case DrawingStandard::JIS: return "JIS B 0001";
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
StandardConfig standard_defaults(DrawingStandard standard) {
|
||||
StandardConfig config;
|
||||
config.standard = standard;
|
||||
|
||||
switch (standard) {
|
||||
case DrawingStandard::ISO:
|
||||
config.text_height = 3.5;
|
||||
config.arrow_size = 3.0;
|
||||
config.extension_line_offset = 1.0;
|
||||
config.extension_line_overshoot = 2.0;
|
||||
config.dimension_line_spacing = 10.0;
|
||||
config.baseline_spacing = 7.0;
|
||||
config.decimal_places = 2;
|
||||
config.trailing_zeros = false;
|
||||
config.leading_zero = true;
|
||||
break;
|
||||
|
||||
case DrawingStandard::ANSI:
|
||||
config.text_height = 3.0; // 1/8 inch ≈ 3.18mm, rounded
|
||||
config.arrow_size = 3.5;
|
||||
config.extension_line_offset = 1.5;
|
||||
config.extension_line_overshoot = 2.5;
|
||||
config.dimension_line_spacing = 10.0;
|
||||
config.baseline_spacing = 10.0;
|
||||
config.decimal_places = 3;
|
||||
config.trailing_zeros = true;
|
||||
config.leading_zero = false;
|
||||
break;
|
||||
|
||||
case DrawingStandard::JIS:
|
||||
config.text_height = 3.5;
|
||||
config.arrow_size = 3.0;
|
||||
config.extension_line_offset = 1.0;
|
||||
config.extension_line_overshoot = 2.0;
|
||||
config.dimension_line_spacing = 8.0;
|
||||
config.baseline_spacing = 7.0;
|
||||
config.decimal_places = 2;
|
||||
config.trailing_zeros = false;
|
||||
config.leading_zero = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// apply_standard
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void apply_standard(DrawingSettings& drawing, DrawingStandard standard) {
|
||||
drawing.config = standard_defaults(standard);
|
||||
}
|
||||
|
||||
void apply_standard_with_overrides(DrawingSettings& drawing,
|
||||
DrawingStandard standard, const StandardConfig& overrides) {
|
||||
drawing.config = standard_defaults(standard);
|
||||
|
||||
// Merge non-default override values
|
||||
if (overrides.text_height > 0) drawing.config.text_height = overrides.text_height;
|
||||
if (overrides.arrow_size > 0) drawing.config.arrow_size = overrides.arrow_size;
|
||||
if (overrides.extension_line_offset > 0) drawing.config.extension_line_offset = overrides.extension_line_offset;
|
||||
if (overrides.extension_line_overshoot > 0) drawing.config.extension_line_overshoot = overrides.extension_line_overshoot;
|
||||
if (overrides.dimension_line_spacing > 0) drawing.config.dimension_line_spacing = overrides.dimension_line_spacing;
|
||||
if (overrides.baseline_spacing > 0) drawing.config.baseline_spacing = overrides.baseline_spacing;
|
||||
if (!overrides.unit.empty()) drawing.config.unit = overrides.unit;
|
||||
if (overrides.decimal_places >= 0) drawing.config.decimal_places = overrides.decimal_places;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// auto_dimension — core implementation
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::vector<Dimension> auto_dimension(
|
||||
const BrepModel& model, const Vector3D& view_direction) {
|
||||
return auto_dimension_with_config(model, view_direction,
|
||||
standard_defaults(DrawingStandard::ISO));
|
||||
}
|
||||
|
||||
std::vector<Dimension> auto_dimension_with_config(
|
||||
const BrepModel& model, const Vector3D& view_direction,
|
||||
const StandardConfig& config) {
|
||||
|
||||
std::vector<Dimension> dims;
|
||||
if (model.num_edges() < 2) return dims;
|
||||
|
||||
Vector3D ndir = view_direction.normalized();
|
||||
auto bb = model.bounds();
|
||||
double total_span = (bb.max() - bb.min()).norm();
|
||||
|
||||
// ── Linear dimensions: parallel edge pairs ──
|
||||
std::set<std::pair<int, int>> processed_pairs;
|
||||
|
||||
for (size_t i = 0; i < model.num_edges(); ++i) {
|
||||
for (size_t j = i + 1; j < model.num_edges(); ++j) {
|
||||
if (edges_parallel(model, static_cast<int>(i), static_cast<int>(j))) {
|
||||
auto pair = std::minmax(i, j);
|
||||
if (processed_pairs.count(pair)) continue;
|
||||
processed_pairs.insert(pair);
|
||||
|
||||
double dist = parallel_edge_distance(model,
|
||||
static_cast<int>(i), static_cast<int>(j));
|
||||
|
||||
// Filter: keep dimensions that matter (> 0.1% of total span)
|
||||
if (dist < total_span * 0.001 || dist < 1e-9) continue;
|
||||
|
||||
Dimension dim;
|
||||
dim.type = DimensionType::Linear;
|
||||
dim.value = dist;
|
||||
dim.associated_edges = {static_cast<int>(i), static_cast<int>(j)};
|
||||
|
||||
// Position text at midpoint between edges
|
||||
Point3D mid0 = edge_midpoint(model, static_cast<int>(i));
|
||||
Point3D mid1 = edge_midpoint(model, static_cast<int>(j));
|
||||
Point3D mid((mid0.x() + mid1.x()) * 0.5,
|
||||
(mid0.y() + mid1.y()) * 0.5,
|
||||
(mid0.z() + mid1.z()) * 0.5);
|
||||
auto [tx, ty] = project_to_view(mid, ndir);
|
||||
dim.text_position = Point3D(tx, ty, 0);
|
||||
|
||||
auto [rx0, ry0] = project_to_view(mid0, ndir);
|
||||
auto [rx1, ry1] = project_to_view(mid1, ndir);
|
||||
dim.reference_points = {
|
||||
Point3D(rx0, ry0, 0),
|
||||
Point3D(rx1, ry1, 0)
|
||||
};
|
||||
|
||||
dims.push_back(dim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Angular dimensions: non-parallel edges ──
|
||||
for (size_t i = 0; i < model.num_edges(); ++i) {
|
||||
for (size_t j = i + 1; j < model.num_edges(); ++j) {
|
||||
if (edges_parallel(model, static_cast<int>(i), static_cast<int>(j)))
|
||||
continue;
|
||||
|
||||
double angle = edge_angle(model, static_cast<int>(i), static_cast<int>(j));
|
||||
|
||||
// Filter: only add if there's a meaningful angle (not near 0 or 180)
|
||||
if (angle < 1.0 || angle > 179.0) continue;
|
||||
|
||||
// Only add one angle per edge pair that matter
|
||||
Dimension dim;
|
||||
dim.type = DimensionType::Angular;
|
||||
dim.value = angle;
|
||||
dim.suffix = "°";
|
||||
dim.associated_edges = {static_cast<int>(i), static_cast<int>(j)};
|
||||
|
||||
Point3D mid0 = edge_midpoint(model, static_cast<int>(i));
|
||||
Point3D mid1 = edge_midpoint(model, static_cast<int>(j));
|
||||
auto [tx, ty] = project_to_view(
|
||||
Point3D((mid0.x() + mid1.x()) * 0.5,
|
||||
(mid0.y() + mid1.y()) * 0.5,
|
||||
(mid0.z() + mid1.z()) * 0.5), ndir);
|
||||
dim.text_position = Point3D(tx, ty, 0);
|
||||
dims.push_back(dim);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Radius / Diameter: arc/circle edges ──
|
||||
for (size_t ei = 0; ei < model.num_edges(); ++ei) {
|
||||
if (!is_arc_edge(model, static_cast<int>(ei))) continue;
|
||||
|
||||
double radius = estimate_radius(model, static_cast<int>(ei));
|
||||
if (radius < total_span * 0.001 || radius < 1e-9) continue;
|
||||
|
||||
Dimension dim;
|
||||
dim.associated_edges = {static_cast<int>(ei)};
|
||||
Point3D mid = edge_midpoint(model, static_cast<int>(ei));
|
||||
auto [tx, ty] = project_to_view(mid, ndir);
|
||||
|
||||
if (is_closed_circle(model, static_cast<int>(ei))) {
|
||||
dim.type = DimensionType::Diameter;
|
||||
dim.value = radius * 2.0;
|
||||
dim.prefix = "Ø";
|
||||
dim.text_position = Point3D(tx + radius, ty, 0);
|
||||
} else {
|
||||
dim.type = DimensionType::Radius;
|
||||
dim.value = radius;
|
||||
dim.prefix = "R";
|
||||
// Place text offset from arc midpoint
|
||||
dim.text_position = Point3D(tx + radius * 0.7, ty + radius * 0.7, 0);
|
||||
}
|
||||
|
||||
dims.push_back(dim);
|
||||
}
|
||||
|
||||
// ── Smart placement: avoid overlap ──
|
||||
smart_placement(dims, ndir);
|
||||
|
||||
return dims;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// smart_placement
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void smart_placement(std::vector<Dimension>& dimensions,
|
||||
const Vector3D& view_direction) {
|
||||
if (dimensions.empty()) return;
|
||||
|
||||
const double min_spacing = 6.0; // minimum text spacing
|
||||
|
||||
// Group by type, then distribute vertically
|
||||
std::map<DimensionType, std::vector<size_t>> groups;
|
||||
for (size_t i = 0; i < dimensions.size(); ++i) {
|
||||
groups[dimensions[i].type].push_back(i);
|
||||
}
|
||||
|
||||
double global_y_offset = 0.0;
|
||||
|
||||
for (auto& [type, indices] : groups) {
|
||||
// Sort indices by text_position y
|
||||
std::sort(indices.begin(), indices.end(),
|
||||
[&](size_t a, size_t b) {
|
||||
return dimensions[a].text_position.y() < dimensions[b].text_position.y();
|
||||
});
|
||||
|
||||
double last_y = -std::numeric_limits<double>::max();
|
||||
for (size_t idx : indices) {
|
||||
double desired_y = dimensions[idx].text_position.y() + global_y_offset;
|
||||
if (desired_y < last_y + min_spacing) {
|
||||
desired_y = last_y + min_spacing;
|
||||
}
|
||||
dimensions[idx].text_position = Point3D(
|
||||
dimensions[idx].text_position.x(),
|
||||
desired_y,
|
||||
dimensions[idx].text_position.z());
|
||||
last_y = desired_y;
|
||||
}
|
||||
|
||||
// Shift next group down
|
||||
if (!indices.empty()) {
|
||||
global_y_offset = last_y + min_spacing * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// baseline_dimensions
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::vector<Dimension> baseline_dimensions(
|
||||
const BrepModel& model, int reference_face_id,
|
||||
const Vector3D& view_direction) {
|
||||
|
||||
std::vector<Dimension> dims;
|
||||
if (reference_face_id < 0 ||
|
||||
reference_face_id >= static_cast<int>(model.num_faces()))
|
||||
return dims;
|
||||
|
||||
Vector3D ndir = view_direction.normalized();
|
||||
|
||||
// Get reference face edges
|
||||
auto ref_face_edges = model.face_edges(reference_face_id);
|
||||
|
||||
// For each other face, create ordinate dimension from reference
|
||||
for (size_t fi = 0; fi < model.num_faces(); ++fi) {
|
||||
if (static_cast<int>(fi) == reference_face_id) continue;
|
||||
|
||||
const auto& face = model.face(static_cast<int>(fi));
|
||||
|
||||
// Find closest edge midpoint distance
|
||||
double min_dist = std::numeric_limits<double>::max();
|
||||
Point3D best_point;
|
||||
for (int edge_id : ref_face_edges) {
|
||||
Point3D ref_pt = edge_midpoint(model, edge_id);
|
||||
|
||||
for (int fe : model.face_edges(static_cast<int>(fi))) {
|
||||
Point3D face_pt = edge_midpoint(model, fe);
|
||||
// Project distance along a consistent direction
|
||||
Vector3D diff(face_pt.x() - ref_pt.x(),
|
||||
face_pt.y() - ref_pt.y(),
|
||||
face_pt.z() - ref_pt.z());
|
||||
double proj_dist = std::abs(diff.dot(ndir));
|
||||
if (proj_dist < min_dist && proj_dist > 1e-9) {
|
||||
min_dist = proj_dist;
|
||||
best_point = face_pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (min_dist < std::numeric_limits<double>::max()) {
|
||||
Dimension dim;
|
||||
dim.type = DimensionType::Ordinate;
|
||||
dim.value = min_dist;
|
||||
dim.associated_faces = {reference_face_id, static_cast<int>(fi)};
|
||||
auto [tx, ty] = project_to_view(best_point, ndir);
|
||||
dim.text_position = Point3D(tx, ty + 10.0, 0);
|
||||
dims.push_back(dim);
|
||||
}
|
||||
}
|
||||
|
||||
return dims;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// chain_dimensions
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::vector<Dimension> chain_dimensions(
|
||||
const BrepModel& model, const std::vector<int>& face_ids,
|
||||
const Vector3D& view_direction) {
|
||||
|
||||
std::vector<Dimension> dims;
|
||||
if (face_ids.size() < 2) return dims;
|
||||
|
||||
Vector3D ndir = view_direction.normalized();
|
||||
|
||||
for (size_t i = 0; i + 1 < face_ids.size(); ++i) {
|
||||
int f0 = face_ids[i];
|
||||
int f1 = face_ids[i + 1];
|
||||
|
||||
if (f0 < 0 || f0 >= static_cast<int>(model.num_faces()) ||
|
||||
f1 < 0 || f1 >= static_cast<int>(model.num_faces()))
|
||||
continue;
|
||||
|
||||
// Find closest edge midpoint pair between adjacent faces
|
||||
double min_dist = std::numeric_limits<double>::max();
|
||||
Point3D best_mid;
|
||||
|
||||
for (int e0 : model.face_edges(f0)) {
|
||||
Point3D p0 = edge_midpoint(model, e0);
|
||||
for (int e1 : model.face_edges(f1)) {
|
||||
Point3D p1 = edge_midpoint(model, e1);
|
||||
double d = (p1 - p0).norm();
|
||||
if (d < min_dist && d > 1e-9) {
|
||||
min_dist = d;
|
||||
best_mid = Point3D((p0.x() + p1.x()) * 0.5,
|
||||
(p0.y() + p1.y()) * 0.5,
|
||||
(p0.z() + p1.z()) * 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (min_dist < std::numeric_limits<double>::max()) {
|
||||
Dimension dim;
|
||||
dim.type = DimensionType::Linear;
|
||||
dim.value = min_dist;
|
||||
dim.associated_faces = {f0, f1};
|
||||
auto [tx, ty] = project_to_view(best_mid, ndir);
|
||||
dim.text_position = Point3D(tx, ty, 0);
|
||||
dims.push_back(dim);
|
||||
}
|
||||
}
|
||||
|
||||
smart_placement(dims, ndir);
|
||||
return dims;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
@@ -0,0 +1,255 @@
|
||||
#include "vde/brep/ffd_deformation.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Bernstein polynomial
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double bernstein(int i, int n, double t) {
|
||||
if (i < 0 || i > n) return 0.0;
|
||||
|
||||
// Clamp t to [0,1]
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
|
||||
// Special cases
|
||||
if (n == 0) return 1.0;
|
||||
if (t == 0.0) return (i == 0) ? 1.0 : 0.0;
|
||||
if (t == 1.0) return (i == n) ? 1.0 : 0.0;
|
||||
|
||||
// Compute binomial coefficient C(n,i)
|
||||
auto binomial = [](int n, int k) -> double {
|
||||
if (k < 0 || k > n) return 0.0;
|
||||
if (k == 0 || k == n) return 1.0;
|
||||
// Use multiplicative formula to avoid overflow
|
||||
double result = 1.0;
|
||||
for (int j = 1; j <= k; ++j) {
|
||||
result *= static_cast<double>(n - k + j) / static_cast<double>(j);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
double C = binomial(n, i);
|
||||
return C * std::pow(t, i) * std::pow(1.0 - t, n - i);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// World-to-lattice parameter space mapping
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Map a world-space point to lattice parameter space (s,t,u)
|
||||
*
|
||||
* Maps linearly from the lattice bounding box to [0,1]³.
|
||||
* Points outside the bbox are clamped.
|
||||
*/
|
||||
void world_to_lattice(const Point3D& p, const FFDLattice& lattice,
|
||||
double& s, double& t, double& u) {
|
||||
double span_x = lattice.bbox.max().x() - lattice.bbox.min().x();
|
||||
double span_y = lattice.bbox.max().y() - lattice.bbox.min().y();
|
||||
double span_z = lattice.bbox.max().z() - lattice.bbox.min().z();
|
||||
|
||||
s = (span_x > 1e-12) ? (p.x() - lattice.bbox.min().x()) / span_x : 0.5;
|
||||
t = (span_y > 1e-12) ? (p.y() - lattice.bbox.min().y()) / span_y : 0.5;
|
||||
u = (span_z > 1e-12) ? (p.z() - lattice.bbox.min().z()) / span_z : 0.5;
|
||||
|
||||
// Clamp to [0,1]
|
||||
s = std::max(0.0, std::min(1.0, s));
|
||||
t = std::max(0.0, std::min(1.0, t));
|
||||
u = std::max(0.0, std::min(1.0, u));
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Lattice creation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
FFDLattice create_lattice(const AABB3D& bbox, int nx, int ny, int nz) {
|
||||
if (nx < 2 || ny < 2 || nz < 2) {
|
||||
throw std::invalid_argument("create_lattice: nx, ny, nz must be >= 2");
|
||||
}
|
||||
|
||||
FFDLattice lattice;
|
||||
lattice.nx = nx;
|
||||
lattice.ny = ny;
|
||||
lattice.nz = nz;
|
||||
lattice.bbox = bbox;
|
||||
lattice.control_points.resize(nx * ny * nz);
|
||||
|
||||
double span_x = bbox.max().x() - bbox.min().x();
|
||||
double span_y = bbox.max().y() - bbox.min().y();
|
||||
double span_z = bbox.max().z() - bbox.min().z();
|
||||
|
||||
double dx = (nx > 1) ? span_x / (nx - 1) : 0.0;
|
||||
double dy = (ny > 1) ? span_y / (ny - 1) : 0.0;
|
||||
double dz = (nz > 1) ? span_z / (nz - 1) : 0.0;
|
||||
|
||||
for (int k = 0; k < nz; ++k) {
|
||||
double z = bbox.min().z() + k * dz;
|
||||
for (int j = 0; j < ny; ++j) {
|
||||
double y = bbox.min().y() + j * dy;
|
||||
for (int i = 0; i < nx; ++i) {
|
||||
double x = bbox.min().x() + i * dx;
|
||||
lattice.control_points[(k * ny + j) * nx + i] = Point3D(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lattice;
|
||||
}
|
||||
|
||||
FFDLattice create_cage_lattice(const std::vector<Point3D>& cage_vertices,
|
||||
int nx, int ny, int nz) {
|
||||
if (cage_vertices.empty()) {
|
||||
throw std::invalid_argument("create_cage_lattice: cage_vertices must not be empty");
|
||||
}
|
||||
if (nx < 2 || ny < 2 || nz < 2) {
|
||||
throw std::invalid_argument("create_cage_lattice: nx, ny, nz must be >= 2");
|
||||
}
|
||||
|
||||
// Compute bounding box from cage vertices
|
||||
AABB3D bbox;
|
||||
for (const auto& v : cage_vertices) {
|
||||
bbox.expand(v);
|
||||
}
|
||||
|
||||
return create_lattice(bbox, nx, ny, nz);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Point deformation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
Point3D deform_point(const Point3D& point,
|
||||
const FFDLattice& lattice,
|
||||
const std::vector<Vector3D>& displacements) {
|
||||
if (!lattice.is_valid()) {
|
||||
throw std::invalid_argument("deform_point: invalid lattice");
|
||||
}
|
||||
if (displacements.size() != lattice.num_points()) {
|
||||
throw std::invalid_argument("deform_point: displacements size mismatch");
|
||||
}
|
||||
|
||||
// Map world point to lattice parameter space
|
||||
double s, t, u;
|
||||
world_to_lattice(point, lattice, s, t, u);
|
||||
|
||||
int nx = lattice.nx;
|
||||
int ny = lattice.ny;
|
||||
int nz = lattice.nz;
|
||||
|
||||
// Evaluate Bernstein volume: sum of B_i(s) * B_j(t) * B_k(u) * (P_ijk + D_ijk)
|
||||
Point3D deformed = Point3D::Zero();
|
||||
double total_weight = 0.0;
|
||||
|
||||
// Precompute Bernstein values for each dimension
|
||||
std::vector<double> Bs(nx), Bt(ny), Bu(nz);
|
||||
for (int i = 0; i < nx; ++i) Bs[i] = bernstein(i, nx - 1, s);
|
||||
for (int j = 0; j < ny; ++j) Bt[j] = bernstein(j, ny - 1, t);
|
||||
for (int k = 0; k < nz; ++k) Bu[k] = bernstein(k, nz - 1, u);
|
||||
|
||||
for (int k = 0; k < nz; ++k) {
|
||||
double Bk = Bu[k];
|
||||
for (int j = 0; j < ny; ++j) {
|
||||
double Bj = Bt[j] * Bk;
|
||||
for (int i = 0; i < nx; ++i) {
|
||||
double w = Bs[i] * Bj;
|
||||
total_weight += w;
|
||||
int idx = (k * ny + j) * nx + i;
|
||||
deformed += w * (lattice.control_points[idx] + displacements[idx]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize (should be ~1.0 for Bezier, but handle edge cases)
|
||||
if (total_weight > 1e-12) {
|
||||
deformed /= total_weight;
|
||||
}
|
||||
|
||||
return deformed;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// B-Rep deformation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
BrepModel deform_brep(const BrepModel& body,
|
||||
const FFDLattice& lattice,
|
||||
const std::vector<Vector3D>& displacements) {
|
||||
if (!lattice.is_valid()) {
|
||||
throw std::invalid_argument("deform_brep: invalid lattice");
|
||||
}
|
||||
if (displacements.size() != lattice.num_points()) {
|
||||
throw std::invalid_argument("deform_brep: displacements size mismatch");
|
||||
}
|
||||
if (!body.is_valid()) {
|
||||
throw std::invalid_argument("deform_brep: input body is not valid");
|
||||
}
|
||||
|
||||
// Copy the body (default copy constructor copies all vectors)
|
||||
BrepModel result = body;
|
||||
|
||||
// Deform each vertex position
|
||||
for (size_t vi = 0; vi < result.num_vertices(); ++vi) {
|
||||
const Point3D& orig = result.vertex(static_cast<int>(vi)).point;
|
||||
Point3D deformed = deform_point(orig, lattice, displacements);
|
||||
result.set_vertex_point(static_cast<int>(vi), deformed);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// Mesh deformation
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
mesh::HalfedgeMesh deform_mesh(const mesh::HalfedgeMesh& mesh,
|
||||
const FFDLattice& lattice,
|
||||
const std::vector<Vector3D>& displacements) {
|
||||
if (!lattice.is_valid()) {
|
||||
throw std::invalid_argument("deform_mesh: invalid lattice");
|
||||
}
|
||||
if (displacements.size() != lattice.num_points()) {
|
||||
throw std::invalid_argument("deform_mesh: displacements size mismatch");
|
||||
}
|
||||
if (mesh.num_vertices() == 0) {
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// We need to reconstruct the mesh since HalfedgeMesh doesn't support
|
||||
// vertex position modification after construction.
|
||||
// Build from triangles approach: extract all vertices and faces,
|
||||
// deform vertices, rebuild.
|
||||
|
||||
std::vector<Point3D> vertices;
|
||||
std::vector<std::array<int, 3>> triangles;
|
||||
|
||||
for (size_t vi = 0; vi < mesh.num_vertices(); ++vi) {
|
||||
Point3D deformed = deform_point(mesh.vertex(vi), lattice, displacements);
|
||||
vertices.push_back(deformed);
|
||||
}
|
||||
|
||||
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
|
||||
auto fv = mesh.face_vertices(static_cast<int>(fi));
|
||||
if (fv.size() >= 3) {
|
||||
// Triangulate polygon (fan triangulation)
|
||||
for (size_t ti = 0; ti + 2 < fv.size(); ++ti) {
|
||||
triangles.push_back({fv[0], fv[ti + 1], fv[ti + 2]});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mesh::HalfedgeMesh result;
|
||||
if (!vertices.empty() && !triangles.empty()) {
|
||||
result.build_from_triangles(vertices, triangles);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
@@ -321,4 +321,492 @@ std::string export_gdt_annotations_dxf(
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GDTToleranceValue
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::string GDTToleranceValue::to_string() const {
|
||||
std::ostringstream ss;
|
||||
|
||||
if (spherical_diameter) ss << "SØ";
|
||||
else if (diameter_symbol) ss << "Ø";
|
||||
|
||||
ss << value;
|
||||
|
||||
switch (material_condition) {
|
||||
case GDTMaterialCondition::MMC: ss << "Ⓜ"; break;
|
||||
case GDTMaterialCondition::LMC: ss << "Ⓛ"; break;
|
||||
case GDTMaterialCondition::RFS: break;
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// DatumReference (enhanced)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::string DatumReference::to_string() const {
|
||||
std::ostringstream ss;
|
||||
ss << label;
|
||||
|
||||
switch (material_condition) {
|
||||
case GDTMaterialCondition::MMC: ss << "Ⓜ"; break;
|
||||
case GDTMaterialCondition::LMC: ss << "Ⓛ"; break;
|
||||
case GDTMaterialCondition::RFS: break;
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GDTCallout methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
static const std::map<GDTType, std::string> kGDTSymbols = {
|
||||
{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, "↗↗"},
|
||||
};
|
||||
|
||||
std::string GDTCallout::symbol() const {
|
||||
auto it = kGDTSymbols.find(type);
|
||||
return it != kGDTSymbols.end() ? it->second : "?";
|
||||
}
|
||||
|
||||
std::string GDTCallout::to_frame_string() const {
|
||||
std::ostringstream ss;
|
||||
|
||||
// Symbol
|
||||
ss << symbol();
|
||||
|
||||
// Tolerance with zone shape
|
||||
ss << " | ";
|
||||
if (zone_shape == GDTFeature::ZoneShape::Cylindrical) ss << "Ø";
|
||||
else if (zone_shape == GDTFeature::ZoneShape::Spherical) ss << "SØ";
|
||||
|
||||
ss << tolerance.to_string();
|
||||
|
||||
// Projected tolerance
|
||||
if (projected_tolerance && projected_height > 0.0) {
|
||||
ss << " Ⓟ" << projected_height;
|
||||
}
|
||||
|
||||
// Datums
|
||||
for (const auto& d : datums) {
|
||||
ss << " | " << d.to_string();
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
GDTFeature GDTCallout::to_feature() const {
|
||||
GDTFeature f;
|
||||
f.type = type;
|
||||
f.tolerance_value = tolerance.value;
|
||||
f.target_face_id = target_face_id;
|
||||
f.zone_shape = zone_shape;
|
||||
f.projected = projected_tolerance;
|
||||
f.projected_height = projected_height;
|
||||
|
||||
f.mmc = (tolerance.material_condition == GDTMaterialCondition::MMC);
|
||||
f.lmc = (tolerance.material_condition == GDTMaterialCondition::LMC);
|
||||
|
||||
for (const auto& d : datums) {
|
||||
DatumReference legacy_dr;
|
||||
legacy_dr.label = d.label;
|
||||
legacy_dr.face_id = d.face_id;
|
||||
legacy_dr.is_primary = d.is_primary;
|
||||
f.datums.push_back(legacy_dr);
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
GDTCallout GDTCallout::from_feature(const GDTFeature& f) {
|
||||
GDTCallout c;
|
||||
c.type = f.type;
|
||||
c.tolerance.value = f.tolerance_value;
|
||||
c.target_face_id = f.target_face_id;
|
||||
c.zone_shape = f.zone_shape;
|
||||
c.projected_tolerance = f.projected;
|
||||
c.projected_height = f.projected_height;
|
||||
|
||||
if (f.mmc) c.tolerance.material_condition = GDTMaterialCondition::MMC;
|
||||
else if (f.lmc) c.tolerance.material_condition = GDTMaterialCondition::LMC;
|
||||
|
||||
for (const auto& d : f.datums) {
|
||||
DatumReference dr;
|
||||
dr.label = d.label;
|
||||
dr.face_id = d.face_id;
|
||||
dr.is_primary = d.is_primary;
|
||||
c.datums.push_back(dr);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// GD&T Frame Builder
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
std::vector<GDTCallout> create_gdt_frame(
|
||||
const BrepModel& model,
|
||||
const std::vector<int>& face_ids,
|
||||
const std::vector<int>& datum_face_ids) {
|
||||
|
||||
std::vector<GDTCallout> callouts;
|
||||
if (face_ids.empty()) return callouts;
|
||||
|
||||
// Build datum references from datum_face_ids
|
||||
std::vector<DatumReference> datums;
|
||||
char label = 'A';
|
||||
for (size_t i = 0; i < datum_face_ids.size() && i < 3; ++i) {
|
||||
DatumReference dr;
|
||||
dr.label = std::string(1, label + static_cast<char>(i));
|
||||
dr.face_id = datum_face_ids[i];
|
||||
dr.is_primary = (i == 0);
|
||||
datums.push_back(dr);
|
||||
}
|
||||
|
||||
// For each target face, generate appropriate GD&T callout based on geometry
|
||||
for (int fid : face_ids) {
|
||||
if (fid < 0 || fid >= static_cast<int>(model.num_faces())) continue;
|
||||
|
||||
// Analyze face geometry to determine appropriate GD&T
|
||||
auto face_edges = model.face_edges(fid);
|
||||
bool is_planar = true; // simplified: assume planar if < 6 edges
|
||||
bool is_cylindrical = (face_edges.size() <= 4); // heuristic
|
||||
|
||||
// Default: Flatness for planar faces, Cylindricity for cylindrical
|
||||
GDTType suggested_type = is_cylindrical ? GDTType::Cylindricity : GDTType::Flatness;
|
||||
|
||||
GDTCallout callout;
|
||||
callout.type = suggested_type;
|
||||
callout.tolerance.value = 0.1; // default tolerance
|
||||
callout.tolerance.material_condition = GDTMaterialCondition::RFS;
|
||||
callout.target_face_id = fid;
|
||||
|
||||
// For location/orientation types, add datum references
|
||||
if (gdt_requires_datum(suggested_type)) {
|
||||
callout.datums = datums;
|
||||
}
|
||||
|
||||
callouts.push_back(callout);
|
||||
}
|
||||
|
||||
// Add perpendicularity/parallelism for face pairs if datums available
|
||||
if (!datums.empty() && face_ids.size() >= 2) {
|
||||
for (size_t i = 0; i + 1 < face_ids.size(); ++i) {
|
||||
GDTCallout callout;
|
||||
callout.type = GDTType::Perpendicularity;
|
||||
callout.tolerance.value = 0.1;
|
||||
callout.target_face_id = face_ids[i + 1];
|
||||
|
||||
DatumReference dr;
|
||||
dr.label = datums[0].label;
|
||||
dr.face_id = face_ids[i];
|
||||
dr.is_primary = true;
|
||||
callout.datums = {dr};
|
||||
|
||||
callouts.push_back(callout);
|
||||
}
|
||||
}
|
||||
|
||||
return callouts;
|
||||
}
|
||||
|
||||
GDTCallout make_gdt_callout(
|
||||
GDTType type, double tolerance_value,
|
||||
const std::vector<DatumReference>& datums, int target_face_id) {
|
||||
|
||||
GDTCallout callout;
|
||||
callout.type = type;
|
||||
callout.tolerance.value = tolerance_value;
|
||||
callout.tolerance.material_condition = GDTMaterialCondition::RFS;
|
||||
callout.datums = datums;
|
||||
callout.target_face_id = target_face_id;
|
||||
|
||||
// Set zone shape based on type
|
||||
switch (type) {
|
||||
case GDTType::Position:
|
||||
case GDTType::Concentricity:
|
||||
callout.zone_shape = GDTFeature::ZoneShape::Cylindrical;
|
||||
callout.tolerance.diameter_symbol = true;
|
||||
break;
|
||||
default:
|
||||
callout.zone_shape = GDTFeature::ZoneShape::ParallelPlanes;
|
||||
break;
|
||||
}
|
||||
|
||||
return callout;
|
||||
}
|
||||
|
||||
bool gdt_requires_datum(GDTType type) {
|
||||
switch (type) {
|
||||
case GDTType::Flatness:
|
||||
case GDTType::Straightness:
|
||||
case GDTType::Circularity:
|
||||
case GDTType::Cylindricity:
|
||||
return false;
|
||||
case GDTType::Parallelism:
|
||||
case GDTType::Perpendicularity:
|
||||
case GDTType::Angularity:
|
||||
case GDTType::Position:
|
||||
case GDTType::Concentricity:
|
||||
case GDTType::Symmetry:
|
||||
case GDTType::ProfileOfLine:
|
||||
case GDTType::ProfileOfSurface:
|
||||
case GDTType::CircularRunout:
|
||||
case GDTType::TotalRunout:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int gdt_expected_datums(GDTType type) {
|
||||
switch (type) {
|
||||
case GDTType::Flatness:
|
||||
case GDTType::Straightness:
|
||||
case GDTType::Circularity:
|
||||
case GDTType::Cylindricity:
|
||||
return 0;
|
||||
case GDTType::Parallelism:
|
||||
case GDTType::Perpendicularity:
|
||||
case GDTType::Angularity:
|
||||
case GDTType::CircularRunout:
|
||||
case GDTType::TotalRunout:
|
||||
case GDTType::ProfileOfLine:
|
||||
case GDTType::ProfileOfSurface:
|
||||
case GDTType::Symmetry:
|
||||
return 1;
|
||||
case GDTType::Position:
|
||||
case GDTType::Concentricity:
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> validate_gdt_callout(
|
||||
const BrepModel& model, const GDTCallout& callout) {
|
||||
|
||||
std::vector<std::string> errors;
|
||||
|
||||
// Check tolerance positivity
|
||||
if (callout.tolerance.value <= 0.0) {
|
||||
errors.push_back("Tolerance value must be positive");
|
||||
}
|
||||
|
||||
// Check datum requirements
|
||||
if (gdt_requires_datum(callout.type) && callout.datums.empty()) {
|
||||
errors.push_back("GD&T type requires at least one datum reference");
|
||||
}
|
||||
|
||||
// Check face ID validity
|
||||
if (callout.target_face_id >= 0 &&
|
||||
callout.target_face_id >= static_cast<int>(model.num_faces())) {
|
||||
errors.push_back("Target face ID out of range");
|
||||
}
|
||||
|
||||
// Check datum face validity
|
||||
for (const auto& d : callout.datums) {
|
||||
if (d.face_id >= 0 &&
|
||||
d.face_id >= static_cast<int>(model.num_faces())) {
|
||||
errors.push_back("Datum face ID out of range: " + d.label);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Additional geometric computations
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
double compute_circularity(const BrepModel& body, int face_id) {
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.size() < 4) return 0.0;
|
||||
|
||||
// Fit best circle (simplified: use centroid + average radius)
|
||||
Point3D center(0, 0, 0);
|
||||
for (const auto& p : pts) center = center + p;
|
||||
center = center * (1.0 / pts.size());
|
||||
|
||||
double avg_r = 0.0;
|
||||
for (const auto& p : pts) avg_r += (p - center).norm();
|
||||
avg_r /= pts.size();
|
||||
|
||||
// Circularity = max deviation from average radius
|
||||
double max_dev = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
double dev = std::abs((p - center).norm() - avg_r);
|
||||
if (dev > max_dev) max_dev = dev;
|
||||
}
|
||||
return max_dev;
|
||||
}
|
||||
|
||||
double compute_cylindricity(const BrepModel& body, int face_id) {
|
||||
// Simplified: measure radial variation along face
|
||||
auto pts = sample_face_points(body, face_id, 100);
|
||||
if (pts.size() < 6) return 0.0;
|
||||
|
||||
// Fit axis (center line through min/max z)
|
||||
double min_z = pts[0].z(), max_z = min_z;
|
||||
for (const auto& p : pts) {
|
||||
if (p.z() < min_z) min_z = p.z();
|
||||
if (p.z() > max_z) max_z = p.z();
|
||||
}
|
||||
|
||||
// Average radius at different heights
|
||||
double max_dev = 0.0;
|
||||
for (size_t i = 0; i < pts.size(); ++i) {
|
||||
double r = std::sqrt(pts[i].x() * pts[i].x() + pts[i].y() * pts[i].y());
|
||||
for (size_t j = i + 1; j < pts.size(); ++j) {
|
||||
double r2 = std::sqrt(pts[j].x() * pts[j].x() + pts[j].y() * pts[j].y());
|
||||
double dev = std::abs(r - r2);
|
||||
if (dev > max_dev) max_dev = dev;
|
||||
}
|
||||
}
|
||||
return max_dev;
|
||||
}
|
||||
|
||||
double compute_straightness(const BrepModel& body, int edge_id) {
|
||||
if (edge_id < 0 || edge_id >= static_cast<int>(body.num_edges())) return 0.0;
|
||||
|
||||
const auto& e = body.edge(edge_id);
|
||||
Point3D start = body.vertex(e.v_start).point;
|
||||
Point3D end = body.vertex(e.v_end).point;
|
||||
|
||||
// For a straight edge with no curve, this is 0
|
||||
if (!e.curve) return 0.0;
|
||||
|
||||
// Sample points along the curve and measure deviation from straight line
|
||||
auto& cp = e.curve->control_points();
|
||||
Vector3D line_dir(end.x() - start.x(), end.y() - start.y(), end.z() - start.z());
|
||||
double line_len = line_dir.norm();
|
||||
if (line_len < 1e-12) return 0.0;
|
||||
line_dir = line_dir * (1.0 / line_len);
|
||||
|
||||
double max_dev = 0.0;
|
||||
for (size_t i = 1; i + 1 < cp.size(); ++i) {
|
||||
Vector3D to_p(cp[i].x() - start.x(), cp[i].y() - start.y(), cp[i].z() - start.z());
|
||||
double proj = to_p.dot(line_dir);
|
||||
Vector3D perp = to_p - line_dir * proj;
|
||||
double dev = perp.norm();
|
||||
if (dev > max_dev) max_dev = dev;
|
||||
}
|
||||
|
||||
return max_dev;
|
||||
}
|
||||
|
||||
double compute_angularity(const BrepModel& body,
|
||||
int face_id, int datum_face_id, double nominal_angle_deg) {
|
||||
|
||||
Vector3D datum_normal = face_normal(body, datum_face_id);
|
||||
auto pts = sample_face_points(body, face_id);
|
||||
if (pts.size() < 3) return 0.0;
|
||||
|
||||
Vector3D face_n = fit_plane(pts).second;
|
||||
|
||||
// Compute actual angle between face normal and datum normal
|
||||
double dot = std::abs(face_n.dot(datum_normal));
|
||||
dot = std::max(-1.0, std::min(1.0, dot));
|
||||
double actual_angle_deg = std::acos(dot) * 180.0 / M_PI;
|
||||
|
||||
// Deviation from nominal
|
||||
return std::abs(actual_angle_deg - nominal_angle_deg);
|
||||
}
|
||||
|
||||
double compute_symmetry(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);
|
||||
|
||||
// Symmetry deviation: how far the target face center is from
|
||||
// the datum face's mid-plane
|
||||
Vector3D datum_n = face_normal(body, datum_face_id);
|
||||
double offset = std::abs((c1 - c2).dot(datum_n));
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
double compute_profile_of_surface(const BrepModel& body, int face_id) {
|
||||
auto pts = sample_face_points(body, face_id, 100);
|
||||
if (pts.size() < 3) return 0.0;
|
||||
|
||||
auto [center, normal] = fit_plane(pts);
|
||||
|
||||
// Deviation from best-fit plane (uniform zone = 2× max deviation)
|
||||
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 * 2.0; // Total profile zone
|
||||
}
|
||||
|
||||
double compute_circular_runout(const BrepModel& body,
|
||||
int face_id, int datum_axis_face_id) {
|
||||
// Simplified: measure radial variation of face points relative to datum axis
|
||||
auto pts = sample_face_points(body, face_id, 100);
|
||||
if (pts.size() < 4) return 0.0;
|
||||
|
||||
// Datum axis center
|
||||
Vector3D axis_center = face_center(body, datum_axis_face_id);
|
||||
Vector3D axis_dir = face_normal(body, datum_axis_face_id);
|
||||
|
||||
double max_dev = 0.0;
|
||||
for (const auto& p : pts) {
|
||||
Vector3D to_p(p.x() - axis_center.x(), p.y() - axis_center.y(), p.z() - axis_center.z());
|
||||
// Component perpendicular to axis
|
||||
double proj = to_p.dot(axis_dir);
|
||||
Vector3D radial = to_p - axis_dir * proj;
|
||||
double r = radial.norm();
|
||||
|
||||
// Runout = max radial variation
|
||||
for (const auto& q : pts) {
|
||||
Vector3D to_q(q.x() - axis_center.x(), q.y() - axis_center.y(), q.z() - axis_center.z());
|
||||
double proj2 = to_q.dot(axis_dir);
|
||||
Vector3D radial2 = to_q - axis_dir * proj2;
|
||||
double dev = std::abs(radial.norm() - radial2.norm());
|
||||
if (dev > max_dev) max_dev = dev;
|
||||
}
|
||||
}
|
||||
|
||||
return max_dev;
|
||||
}
|
||||
|
||||
double compute_total_runout(const BrepModel& body,
|
||||
int face_id, int datum_axis_face_id) {
|
||||
// Total runout = circular runout + axial variation
|
||||
double circular = compute_circular_runout(body, face_id, datum_axis_face_id);
|
||||
|
||||
auto pts = sample_face_points(body, face_id, 100);
|
||||
if (pts.size() < 4) return circular;
|
||||
|
||||
Vector3D axis_dir = face_normal(body, datum_axis_face_id);
|
||||
|
||||
// Measure axial variation
|
||||
double min_axial = std::numeric_limits<double>::max();
|
||||
double max_axial = -std::numeric_limits<double>::max();
|
||||
for (const auto& p : pts) {
|
||||
double axial = Vector3D(p.x(), p.y(), p.z()).dot(axis_dir);
|
||||
if (axial < min_axial) min_axial = axial;
|
||||
if (axial > max_axial) max_axial = axial;
|
||||
}
|
||||
|
||||
// Total = radial + axial
|
||||
return circular + (max_axial - min_axial);
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
#include "vde/brep/pmi_mbd.h"
|
||||
#include "vde/brep/step_export.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
|
||||
namespace vde::brep {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PMIAnnotation methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
static const std::map<PMIType, std::string> kPMITypeNames = {
|
||||
{PMIType::Dimension, "DIMENSION"},
|
||||
{PMIType::GDTCalloutType,"GDT"},
|
||||
{PMIType::DatumTarget, "DATUM_TARGET"},
|
||||
{PMIType::SurfaceFinish, "SURFACE_FINISH"},
|
||||
{PMIType::Note, "NOTE"},
|
||||
{PMIType::WeldSymbol, "WELD"},
|
||||
{PMIType::DatumFeature, "DATUM_FEATURE"},
|
||||
{PMIType::Balloon, "BALLOON"},
|
||||
{PMIType::Custom, "CUSTOM"},
|
||||
};
|
||||
|
||||
std::string PMIAnnotation::type_name() const {
|
||||
auto it = kPMITypeNames.find(type);
|
||||
return it != kPMITypeNames.end() ? it->second : "UNKNOWN";
|
||||
}
|
||||
|
||||
std::string PMIAnnotation::to_step_ap242(int& next_id) const {
|
||||
std::ostringstream ss;
|
||||
|
||||
// STEP AP242 uses ANNOTATION_OCCURRENCE and related entities
|
||||
// Simplified representation using DRAUGHTING_MODEL / ANNOTATION_CURVE
|
||||
|
||||
int anno_id = next_id++;
|
||||
int text_id = next_id++;
|
||||
int curve_id = next_id++;
|
||||
int plane_id = next_id++;
|
||||
|
||||
// Placement plane for annotation
|
||||
int cart_pt_id = next_id++;
|
||||
int dir_id = next_id++;
|
||||
int axis_id = next_id++;
|
||||
|
||||
ss << "/* PMI Annotation: " << label << " [" << type_name() << "] */\n";
|
||||
|
||||
// Cartesian point for text position
|
||||
ss << "#" << cart_pt_id << " = CARTESIAN_POINT('ANNOTATION_ORIGIN',("
|
||||
<< text_position.x() << "," << text_position.y() << ","
|
||||
<< text_position.z() << "));\n";
|
||||
|
||||
// Direction for annotation normal
|
||||
ss << "#" << dir_id << " = DIRECTION('ANNOTATION_NORMAL',("
|
||||
<< annotation_normal.x() << "," << annotation_normal.y() << ","
|
||||
<< annotation_normal.z() << "));\n";
|
||||
|
||||
// Axis placement for annotation
|
||||
ss << "#" << axis_id
|
||||
<< " = AXIS2_PLACEMENT_3D('ANNOTATION_PLACEMENT',#"
|
||||
<< cart_pt_id << ",#" << dir_id << ",$,.F.);\n";
|
||||
|
||||
// Annotation plane
|
||||
ss << "#" << plane_id
|
||||
<< " = PLANE('ANNOTATION_PLANE',#" << axis_id << ");\n";
|
||||
|
||||
// Annotation occurrence (generic PMI container)
|
||||
ss << "#" << anno_id
|
||||
<< " = ANNOTATION_OCCURRENCE('" << label << "','" << content
|
||||
<< "',#" << plane_id << ");\n";
|
||||
|
||||
// Annotation text
|
||||
ss << "#" << text_id
|
||||
<< " = ANNOTATION_TEXT('" << content << "',#" << plane_id
|
||||
<< ",.F.,.T.);\n";
|
||||
|
||||
// Annotation curve (for leader/extension lines)
|
||||
ss << "#" << curve_id
|
||||
<< " = ANNOTATION_CURVE('" << label << "_LEADER',#" << plane_id
|
||||
<< ",.F.,.F.);\n";
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PMIView methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
void PMIView::add_annotation(int annotation_id) {
|
||||
visible_ids_.insert(annotation_id);
|
||||
}
|
||||
|
||||
void PMIView::remove_annotation(int annotation_id) {
|
||||
visible_ids_.erase(annotation_id);
|
||||
}
|
||||
|
||||
bool PMIView::is_visible(int annotation_id) const {
|
||||
return visible_ids_.count(annotation_id) > 0;
|
||||
}
|
||||
|
||||
void PMIView::set_all_visible(const std::vector<int>& all_ids) {
|
||||
visible_ids_.clear();
|
||||
for (int id : all_ids) visible_ids_.insert(id);
|
||||
}
|
||||
|
||||
void PMIView::clear_all() {
|
||||
visible_ids_.clear();
|
||||
}
|
||||
|
||||
void PMIView::hide_annotations(const std::vector<int>& ids) {
|
||||
for (int id : ids) visible_ids_.erase(id);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// PMIModel methods
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
int PMIModel::attach_pmi(const PMIAnnotation& annotation) {
|
||||
PMIAnnotation copy = annotation;
|
||||
copy.id = next_annotation_id_++;
|
||||
annotations_.push_back(std::move(copy));
|
||||
return annotations_.back().id;
|
||||
}
|
||||
|
||||
int PMIModel::attach_pmi(PMIAnnotation&& annotation) {
|
||||
annotation.id = next_annotation_id_++;
|
||||
annotations_.push_back(std::move(annotation));
|
||||
return annotations_.back().id;
|
||||
}
|
||||
|
||||
bool PMIModel::remove_pmi(int annotation_id) {
|
||||
auto it = std::find_if(annotations_.begin(), annotations_.end(),
|
||||
[annotation_id](const PMIAnnotation& a) { return a.id == annotation_id; });
|
||||
if (it == annotations_.end()) return false;
|
||||
annotations_.erase(it);
|
||||
|
||||
// Remove from all views
|
||||
for (auto& [name, view] : views_) {
|
||||
view.remove_annotation(annotation_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const PMIAnnotation* PMIModel::get_annotation(int id) const {
|
||||
for (const auto& a : annotations_) {
|
||||
if (a.id == id) return &a;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<const PMIAnnotation*> PMIModel::annotations_for_face(int face_id) const {
|
||||
std::vector<const PMIAnnotation*> result;
|
||||
for (const auto& a : annotations_) {
|
||||
if (std::find(a.associated_faces.begin(), a.associated_faces.end(), face_id)
|
||||
!= a.associated_faces.end()) {
|
||||
result.push_back(&a);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<const PMIAnnotation*> PMIModel::annotations_for_edge(int edge_id) const {
|
||||
std::vector<const PMIAnnotation*> result;
|
||||
for (const auto& a : annotations_) {
|
||||
if (std::find(a.associated_edges.begin(), a.associated_edges.end(), edge_id)
|
||||
!= a.associated_edges.end()) {
|
||||
result.push_back(&a);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<const PMIAnnotation*> PMIModel::annotations_of_type(PMIType type) const {
|
||||
std::vector<const PMIAnnotation*> result;
|
||||
for (const auto& a : annotations_) {
|
||||
if (a.type == type) result.push_back(&a);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PMIModel::set_view(const std::string& name, const PMIView& view) {
|
||||
views_[name] = view;
|
||||
}
|
||||
|
||||
void PMIModel::set_view(const std::string& name, PMIView&& view) {
|
||||
views_[name] = std::move(view);
|
||||
}
|
||||
|
||||
const PMIView* PMIModel::get_view(const std::string& name) const {
|
||||
auto it = views_.find(name);
|
||||
return it != views_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void PMIModel::remove_view(const std::string& name) {
|
||||
views_.erase(name);
|
||||
}
|
||||
|
||||
void PMIModel::create_default_views() {
|
||||
PMIView front("front");
|
||||
front.set_direction(Vector3D(0, 0, -1));
|
||||
|
||||
PMIView top("top");
|
||||
top.set_direction(Vector3D(0, -1, 0));
|
||||
|
||||
PMIView right("right");
|
||||
right.set_direction(Vector3D(-1, 0, 0));
|
||||
|
||||
PMIView iso("iso");
|
||||
iso.set_direction(Vector3D(-1, -1, -1).normalized());
|
||||
|
||||
// Make all annotations visible in all default views
|
||||
std::vector<int> all_ids;
|
||||
for (const auto& a : annotations_) {
|
||||
all_ids.push_back(a.id);
|
||||
}
|
||||
front.set_all_visible(all_ids);
|
||||
top.set_all_visible(all_ids);
|
||||
right.set_all_visible(all_ids);
|
||||
iso.set_all_visible(all_ids);
|
||||
|
||||
views_["front"] = std::move(front);
|
||||
views_["top"] = std::move(top);
|
||||
views_["right"] = std::move(right);
|
||||
views_["iso"] = std::move(iso);
|
||||
}
|
||||
|
||||
std::string PMIModel::to_step_ap242() const {
|
||||
std::ostringstream ss;
|
||||
int next_id = 10000; // Start IDs high to avoid collision with geometry
|
||||
|
||||
ss << "/* =================================================== */\n";
|
||||
ss << "/* PMI / MBD Annotations (STEP AP242) */\n";
|
||||
ss << "/* Total annotations: " << annotations_.size() << " */\n";
|
||||
ss << "/* =================================================== */\n\n";
|
||||
|
||||
// Annotation occurrences
|
||||
ss << "/* --- Annotation Occurrences --- */\n";
|
||||
for (const auto& ann : annotations_) {
|
||||
ss << ann.to_step_ap242(next_id);
|
||||
}
|
||||
|
||||
ss << "\n/* --- PMI Views --- */\n";
|
||||
for (const auto& [name, view] : views_) {
|
||||
int view_id = next_id++;
|
||||
ss << "#" << view_id
|
||||
<< " = DRAUGHTING_MODEL('PMI_VIEW_" << name << "','"
|
||||
<< name << "',#,"
|
||||
<< view.direction().x() << ","
|
||||
<< view.direction().y() << ","
|
||||
<< view.direction().z() << ");\n";
|
||||
|
||||
int pres_id = next_id++;
|
||||
ss << "#" << pres_id
|
||||
<< " = PRESENTATION_VIEW('" << name << "_PRES',#" << view_id << ");\n";
|
||||
}
|
||||
|
||||
ss << "\n/* --- End PMI Section --- */\n";
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
size_t PMIModel::step_ap242_byte_count() const {
|
||||
return to_step_ap242().size();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Top-level API
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
int attach_pmi(BrepModel& body, const PMIAnnotation& annotation,
|
||||
PMIModel& pmi_model) {
|
||||
return pmi_model.attach_pmi(annotation);
|
||||
}
|
||||
|
||||
std::string export_pmi_step_ap242(
|
||||
const BrepModel& body, const PMIModel& pmi_model) {
|
||||
|
||||
// Get base STEP geometry
|
||||
std::string base_step = export_step({body});
|
||||
|
||||
// Insert PMI section before ENDSEC
|
||||
std::string pmi_section = pmi_model.to_step_ap242();
|
||||
|
||||
// Find ENDSEC; to insert PMI entities before it
|
||||
auto endsec_pos = base_step.rfind("ENDSEC;");
|
||||
if (endsec_pos != std::string::npos) {
|
||||
base_step.insert(endsec_pos, pmi_section);
|
||||
} else {
|
||||
// Fallback: append after DATA section
|
||||
base_step += pmi_section;
|
||||
}
|
||||
|
||||
return base_step;
|
||||
}
|
||||
|
||||
void export_pmi_step_ap242_file(const std::string& filepath,
|
||||
const BrepModel& body, const PMIModel& pmi_model) {
|
||||
|
||||
std::string step_content = export_pmi_step_ap242(body, pmi_model);
|
||||
std::ofstream out(filepath);
|
||||
if (out.is_open()) {
|
||||
out << step_content;
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
int auto_generate_pmi(const BrepModel& body, PMIModel& pmi_model,
|
||||
DrawingStandard standard) {
|
||||
int count = 0;
|
||||
|
||||
// Auto-generate dimensions as PMI
|
||||
auto dims = auto_dimension(body, Vector3D(0, 0, -1));
|
||||
for (const auto& dim : dims) {
|
||||
PMIAnnotation ann;
|
||||
ann.type = PMIType::Dimension;
|
||||
ann.label = "DIM_" + std::to_string(count + 1);
|
||||
ann.content = dim.to_text();
|
||||
ann.text_position = dim.text_position;
|
||||
ann.nominal_value = dim.value;
|
||||
ann.tolerance_upper = dim.tolerance_upper;
|
||||
ann.tolerance_lower = dim.tolerance_lower;
|
||||
ann.associated_edges = dim.associated_edges;
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
|
||||
// Set attachment point from first reference point
|
||||
if (!dim.reference_points.empty()) {
|
||||
ann.attachment_point = dim.reference_points[0];
|
||||
}
|
||||
|
||||
pmi_model.attach_pmi(ann);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Auto-generate GD&T PMI from faces
|
||||
std::vector<int> face_ids;
|
||||
for (size_t i = 0; i < body.num_faces(); ++i) {
|
||||
face_ids.push_back(static_cast<int>(i));
|
||||
}
|
||||
|
||||
auto callouts = create_gdt_frame(body, face_ids);
|
||||
for (const auto& callout : callouts) {
|
||||
PMIAnnotation ann;
|
||||
ann.type = PMIType::GDTCalloutType;
|
||||
ann.label = "GDT_" + std::to_string(count + 1);
|
||||
ann.content = callout.to_frame_string();
|
||||
ann.gdt_type = callout.type;
|
||||
ann.datum_refs = callout.datums;
|
||||
ann.associated_faces = {callout.target_face_id};
|
||||
|
||||
// Position near the target face
|
||||
ann.text_position = Point3D(0, 0, static_cast<double>(count) * 5.0);
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
|
||||
pmi_model.attach_pmi(ann);
|
||||
count++;
|
||||
}
|
||||
|
||||
pmi_model.create_default_views();
|
||||
return count;
|
||||
}
|
||||
|
||||
PMIAnnotation make_pmi_dimension(
|
||||
PMIType type, double value,
|
||||
const Point3D& position,
|
||||
const std::vector<int>& face_ids) {
|
||||
|
||||
PMIAnnotation ann;
|
||||
ann.type = type;
|
||||
ann.nominal_value = value;
|
||||
ann.text_position = position;
|
||||
ann.associated_faces = face_ids;
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << std::fixed << std::setprecision(3) << value;
|
||||
ann.content = ss.str();
|
||||
ann.label = "DIM";
|
||||
|
||||
return ann;
|
||||
}
|
||||
|
||||
PMIAnnotation make_pmi_gdt(
|
||||
const GDTCallout& callout,
|
||||
const Point3D& position) {
|
||||
|
||||
PMIAnnotation ann;
|
||||
ann.type = PMIType::GDTCalloutType;
|
||||
ann.content = callout.to_frame_string();
|
||||
ann.text_position = position;
|
||||
ann.gdt_type = callout.type;
|
||||
ann.datum_refs = callout.datums;
|
||||
ann.associated_faces = {callout.target_face_id};
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
ann.label = "GDT";
|
||||
|
||||
return ann;
|
||||
}
|
||||
|
||||
PMIAnnotation make_pmi_surface_finish(
|
||||
double roughness_ra, int face_id,
|
||||
const Point3D& position) {
|
||||
|
||||
PMIAnnotation ann;
|
||||
ann.type = PMIType::SurfaceFinish;
|
||||
ann.content = "Ra " + std::to_string(roughness_ra);
|
||||
ann.text_position = position;
|
||||
ann.associated_faces = {face_id};
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
|
||||
std::ostringstream ss;
|
||||
ss << "SF_Ra" << roughness_ra << "_F" << face_id;
|
||||
ann.label = ss.str();
|
||||
|
||||
return ann;
|
||||
}
|
||||
|
||||
PMIAnnotation make_pmi_datum_target(
|
||||
const std::string& label, int face_id,
|
||||
const Point3D& position) {
|
||||
|
||||
PMIAnnotation ann;
|
||||
ann.type = PMIType::DatumTarget;
|
||||
ann.content = label;
|
||||
ann.text_position = position;
|
||||
ann.associated_faces = {face_id};
|
||||
ann.annotation_normal = Vector3D(0, 0, 1);
|
||||
ann.label = "DATUM_" + label;
|
||||
|
||||
return ann;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
Reference in New Issue
Block a user