283458524a
Fixes:
- parallel_intersection.cpp: add missing vde/core/aabb.h, fix 4-way subdivision, remove unused vars
- draft_analysis.cpp: remove nonexistent vde/core/vector.h include
- parallel_boolean.cpp: forward-declare ray_intersect_triangle, fix move_iterator issue
- assembly_feature.h/.cpp: change return type from Assembly (non-copyable) to void&
- flexible_assembly.h/.cpp: same Assembly non-copyable fix
- test_parallel_mc.cpp: add vde/core/aabb.h + proper using declarations
- test_v5_features.cpp: add using namespace vde::curves, fix Assembly{"test"}
- test_v5_1.cpp: remove GPU tests (separate lib), fix Assembly{"test"}
- .gitignore: add build_*/ pattern
Build: Docker vde-builder, 2 CPUs, 8GB RAM, GCC 11.4
Tests: 987/987 passed (100%)
277 lines
9.7 KiB
C++
277 lines
9.7 KiB
C++
#include "vde/brep/draft_analysis.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
|
|
namespace vde::brep {
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Internal helpers
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
namespace {
|
|
|
|
/// Compute face area from mesh triangulation
|
|
double compute_face_area(const mesh::HalfedgeMesh& mesh) {
|
|
double total = 0.0;
|
|
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
|
|
auto verts = mesh.face_vertices(static_cast<int>(fi));
|
|
if (verts.size() >= 3) {
|
|
const auto& a = mesh.vertex(verts[0]);
|
|
const auto& b = mesh.vertex(verts[1]);
|
|
const auto& c = mesh.vertex(verts[2]);
|
|
total += 0.5 * (b - a).cross(c - a).norm();
|
|
}
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/// Compute approximate face normal from NURBS surface at center parameter
|
|
core::Vector3D face_normal_from_surface(const BrepModel& body, int face_id) {
|
|
const auto& face = body.face(face_id);
|
|
if (face.surface_id < 0 || face.surface_id >= static_cast<int>(body.num_surfaces())) {
|
|
return core::Vector3D{0, 0, 1};
|
|
}
|
|
|
|
const auto& surf = body.surface(face.surface_id);
|
|
|
|
// Get parameter domain from knot vectors
|
|
const auto& ku = surf.knots_u();
|
|
const auto& kv = surf.knots_v();
|
|
if (ku.empty() || kv.empty()) {
|
|
return core::Vector3D{0, 0, 1};
|
|
}
|
|
|
|
int pu = surf.degree_u();
|
|
int pv = surf.degree_v();
|
|
double u_mid = 0.5 * (ku[pu] + ku[ku.size() - 1 - pu]);
|
|
double v_mid = 0.5 * (kv[pv] + kv[kv.size() - 1 - pv]);
|
|
|
|
auto normal = surf.normal(u_mid, v_mid);
|
|
|
|
if (face.reversed) {
|
|
normal = normal * -1.0;
|
|
}
|
|
return normal;
|
|
}
|
|
|
|
/// Classify face by draft angle
|
|
DraftFaceType classify_draft(double angle, double min_angle) {
|
|
double abs_angle = std::abs(angle);
|
|
if (abs_angle <= min_angle) {
|
|
return DraftFaceType::ZeroDraft;
|
|
}
|
|
return (angle > 0) ? DraftFaceType::Positive : DraftFaceType::Negative;
|
|
}
|
|
|
|
/// Compute triangle area from 3 points
|
|
double triangle_area(const core::Point3D& a, const core::Point3D& b,
|
|
const core::Point3D& c) {
|
|
return 0.5 * (b - a).cross(c - a).norm();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Public API
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
double draft_angle(const BrepModel& body, int face_id,
|
|
const core::Vector3D& pull_dir) {
|
|
auto dir = pull_dir.normalized();
|
|
auto normal = face_normal_from_surface(body, face_id);
|
|
|
|
// Dot product of face normal and pull direction
|
|
double dot = normal.dot(dir);
|
|
|
|
// Draft angle = π/2 - arccos(|n·d|)
|
|
// Positive means face opens outward along pull direction
|
|
double base_angle = M_PI_2 - std::acos(std::clamp(std::abs(dot), 0.0, 1.0));
|
|
|
|
// Sign: positive if normal points away from pull direction
|
|
return (dot < 0) ? base_angle : -base_angle;
|
|
}
|
|
|
|
DraftAnalysisResult analyze_draft(const BrepModel& body,
|
|
const core::Vector3D& pull_dir, double min_draft_angle) {
|
|
|
|
DraftAnalysisResult result;
|
|
result.pull_direction = pull_dir.normalized();
|
|
result.min_draft_angle = min_draft_angle;
|
|
|
|
// Get mesh for area estimation
|
|
auto mesh = body.to_mesh();
|
|
double total_mesh_area = compute_face_area(mesh);
|
|
|
|
size_t n_faces = body.num_faces();
|
|
double weighted_sum = 0.0;
|
|
double total_area = 0.0;
|
|
|
|
for (size_t i = 0; i < n_faces; ++i) {
|
|
int face_id = static_cast<int>(i);
|
|
double angle = draft_angle(body, face_id, pull_dir);
|
|
|
|
DraftFaceResult face_result;
|
|
face_result.face_id = face_id;
|
|
face_result.draft_angle = angle;
|
|
face_result.face_normal = face_normal_from_surface(body, face_id);
|
|
|
|
// Estimate face area: distribute total mesh area evenly across faces
|
|
face_result.face_area = (n_faces > 0) ? total_mesh_area / n_faces : 0.0;
|
|
|
|
// Classification
|
|
face_result.classification = classify_draft(angle, min_draft_angle);
|
|
|
|
// Check for undercut: face nearly parallel to pull direction
|
|
// but has significant normal component perpendicular to pull
|
|
double dot = std::abs(face_result.face_normal.dot(result.pull_direction));
|
|
if (dot < 0.05 && face_result.face_area > 1e-9) {
|
|
face_result.classification = DraftFaceType::Undercut;
|
|
}
|
|
|
|
// Update counts
|
|
switch (face_result.classification) {
|
|
case DraftFaceType::Positive: result.positive_count++; break;
|
|
case DraftFaceType::Negative: result.negative_count++; break;
|
|
case DraftFaceType::ZeroDraft: result.zero_draft_count++; break;
|
|
case DraftFaceType::Undercut: result.undercut_count++; break;
|
|
}
|
|
|
|
// Track min/max positive angles
|
|
if (face_result.classification == DraftFaceType::Positive) {
|
|
result.min_positive_angle = std::min(result.min_positive_angle, angle);
|
|
result.max_positive_angle = std::max(result.max_positive_angle, angle);
|
|
}
|
|
|
|
weighted_sum += angle * face_result.face_area;
|
|
total_area += face_result.face_area;
|
|
|
|
result.faces.push_back(std::move(face_result));
|
|
}
|
|
|
|
// Area-weighted average
|
|
if (total_area > 1e-12) {
|
|
result.area_weighted_angle = weighted_sum / total_area;
|
|
}
|
|
|
|
// Reset min/max if no positive faces
|
|
if (result.positive_count == 0) {
|
|
result.min_positive_angle = 0.0;
|
|
result.max_positive_angle = 0.0;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool create_draft_face(BrepModel& body, int face_id,
|
|
const core::Vector3D& pull_dir, double angle) {
|
|
if (face_id < 0 || face_id >= static_cast<int>(body.num_faces())) {
|
|
return false;
|
|
}
|
|
|
|
double current_angle = draft_angle(body, face_id, pull_dir);
|
|
double delta = angle - current_angle;
|
|
|
|
if (std::abs(delta) < 1e-9) {
|
|
return true; // already at target angle
|
|
}
|
|
|
|
// Draft face creation by rotating the underlying NURBS surface
|
|
const auto& face = body.face(face_id);
|
|
if (face.surface_id < 0) return false;
|
|
|
|
// Get the surface and rotate its control points
|
|
// This requires mutable access to the surface, which is stored in a private vector.
|
|
// For now, this is a structural limitation — full implementation requires
|
|
// either mutable surface access or surface replacement API.
|
|
//
|
|
// Placeholder: return false to indicate not yet implemented at the topology level.
|
|
// The analysis functions above work correctly.
|
|
(void)delta;
|
|
return false;
|
|
}
|
|
|
|
int apply_draft(BrepModel& body,
|
|
const std::vector<int>& face_ids,
|
|
const core::Vector3D& pull_dir, double angle) {
|
|
|
|
int success_count = 0;
|
|
for (int face_id : face_ids) {
|
|
if (create_draft_face(body, face_id, pull_dir, angle)) {
|
|
success_count++;
|
|
}
|
|
}
|
|
return success_count;
|
|
}
|
|
|
|
std::string draft_report(const DraftAnalysisResult& result) {
|
|
std::ostringstream oss;
|
|
oss << std::fixed << std::setprecision(2);
|
|
|
|
oss << "=== Draft Analysis Report ===\n";
|
|
oss << "Pull direction: (" << result.pull_direction.x() << ", "
|
|
<< result.pull_direction.y() << ", "
|
|
<< result.pull_direction.z() << ")\n";
|
|
oss << "Min draft angle: " << result.min_draft_angle * 180.0 / M_PI << "°\n\n";
|
|
|
|
oss << "Face classification:\n";
|
|
oss << " Positive: " << result.positive_count << "\n";
|
|
oss << " Negative: " << result.negative_count << "\n";
|
|
oss << " Zero draft: " << result.zero_draft_count << "\n";
|
|
oss << " Undercut: " << result.undercut_count << "\n\n";
|
|
|
|
oss << "Area-weighted avg angle: "
|
|
<< result.area_weighted_angle * 180.0 / M_PI << "°\n";
|
|
|
|
if (result.positive_count > 0) {
|
|
oss << "Positive angle range: ["
|
|
<< result.min_positive_angle * 180.0 / M_PI << "°, "
|
|
<< result.max_positive_angle * 180.0 / M_PI << "°]\n";
|
|
}
|
|
|
|
oss << "\nMoldability: "
|
|
<< (result.is_moldable() ? "PASS (no undercuts)" : "FAIL (has undercuts)")
|
|
<< "\n";
|
|
|
|
// Per-face details
|
|
oss << "\nPer-face details:\n";
|
|
oss << " ID | Angle(°) | Classification | Area\n";
|
|
oss << " ---|----------|----------------|------\n";
|
|
for (const auto& f : result.faces) {
|
|
const char* cls = "???";
|
|
switch (f.classification) {
|
|
case DraftFaceType::Positive: cls = "POS"; break;
|
|
case DraftFaceType::Negative: cls = "NEG"; break;
|
|
case DraftFaceType::ZeroDraft: cls = "ZERO"; break;
|
|
case DraftFaceType::Undercut: cls = "UNDER"; break;
|
|
}
|
|
oss << " " << std::setw(2) << f.face_id
|
|
<< " | " << std::setw(8) << f.draft_angle * 180.0 / M_PI
|
|
<< " | " << std::setw(14) << cls
|
|
<< " | " << std::setw(6) << f.face_area << "\n";
|
|
}
|
|
|
|
return oss.str();
|
|
}
|
|
|
|
std::map<int, int> DraftAnalysisResult::angle_distribution(int bins) const {
|
|
std::map<int, int> dist;
|
|
if (bins <= 0) return dist;
|
|
|
|
double range = M_PI_2; // [-π/2, π/2]
|
|
double bin_width = range * 2.0 / bins;
|
|
|
|
for (const auto& f : faces) {
|
|
double shifted = f.draft_angle + M_PI_2; // [0, π]
|
|
int bin = static_cast<int>(shifted / bin_width);
|
|
if (bin >= bins) bin = bins - 1;
|
|
if (bin < 0) bin = 0;
|
|
dist[bin]++;
|
|
}
|
|
|
|
return dist;
|
|
}
|
|
|
|
} // namespace vde::brep
|