Files
ViewDesignEngine/src/mesh/mesh_to_curve.cpp
T
茂之钳 d949da2719
CI / Build & Test (push) Failing after 32s
CI / Release Build (push) Failing after 31s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled
fix(v1.0.1): restore contour loop closing point for better LS fitting
The contour extraction previously relied on LS fitting to handle
loop closure internally, but removing the closing point caused
fitting quality issues. Restore comp.push_back(comp[0]) to ensure
closed curves are properly fitted.

Related: VDE-021 mesh contour extraction
2026-07-28 18:21:01 +08:00

482 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "vde/mesh/mesh_to_curve.h"
#include <algorithm>
#include <cmath>
#include <map>
#include <set>
#include <unordered_map>
#include <cassert>
namespace vde::mesh {
using core::Point3D;
using core::Vector3D;
namespace {
// ── Helper: chord-length parameterization of polyline ──
std::vector<double> chord_length_params(const std::vector<Point3D>& pts) {
std::vector<double> t(pts.size(), 0.0);
double total = 0.0;
for (size_t i = 1; i < pts.size(); ++i)
total += (pts[i] - pts[i-1]).norm();
if (total < 1e-15) {
for (size_t i = 0; i < pts.size(); ++i)
t[i] = static_cast<double>(i) / (pts.size() - 1);
return t;
}
for (size_t i = 1; i < pts.size(); ++i)
t[i] = t[i-1] + (pts[i] - pts[i-1]).norm() / total;
return t;
}
// ── Helper: uniform knot vector for degree p, n control points ──
std::vector<double> make_uniform_knots(int n, int p) {
int m = n + p + 1;
std::vector<double> knots(m, 0.0);
for (int i = 0; i < p + 1; ++i) knots[i] = 0.0;
for (int i = n; i < m; ++i) knots[i] = 1.0;
int inner = m - 2 * (p + 1);
for (int i = 0; i < inner; ++i)
knots[p + 1 + i] = static_cast<double>(i + 1) / (inner + 1);
return knots;
}
// ── B-spline basis function N_{i,p}(t) ──
double basis_function(int i, int p, const std::vector<double>& knots, double t) {
if (p == 0) {
return (t >= knots[i] && t < knots[i+1]) ||
(t >= knots[i] && t <= knots[i+1] &&
i + 1 == static_cast<int>(knots.size()) - p - 1) ? 1.0 : 0.0;
}
double left = 0.0, right = 0.0;
double denom1 = knots[i+p] - knots[i];
double denom2 = knots[i+p+1] - knots[i+1];
if (denom1 > 1e-15)
left = (t - knots[i]) / denom1 * basis_function(i, p-1, knots, t);
if (denom2 > 1e-15)
right = (knots[i+p+1] - t) / denom2 * basis_function(i+1, p-1, knots, t);
return left + right;
}
// ── Least-squares B-spline fit ──
// Given m sample points with parameters t, compute n control points (degree=p)
// Solve: N^T N CP = N^T P (m×n linear system)
curves::NurbsCurve ls_fit_bspline(const std::vector<Point3D>& samples,
const std::vector<double>& params,
int degree, int num_ctrl) {
int m = static_cast<int>(samples.size());
if (m < 2 || num_ctrl < degree + 1) {
// Degenerate: return empty curve
return curves::NurbsCurve();
}
int n = std::max(degree + 1, num_ctrl);
n = std::min(n, m); // Can't have more ctrl points than samples
auto knots = make_uniform_knots(n, degree);
// Build normal equations: A^T A x = A^T b
// A_ij = N_{j,degree}(t_i)
// For points, we solve 3 independent systems (x, y, z)
std::vector<std::vector<double>> ATA(n, std::vector<double>(n, 0.0));
std::vector<Point3D> ATb(n, Point3D(0, 0, 0));
for (int i = 0; i < m; ++i) {
double t = params[i];
// Evaluate all non-zero basis functions at t
std::vector<double> N(n, 0.0);
for (int j = 0; j < n; ++j)
N[j] = basis_function(j, degree, knots, t);
for (int r = 0; r < n; ++r) {
if (std::abs(N[r]) < 1e-15) continue;
for (int c = 0; c < n; ++c) {
if (std::abs(N[c]) < 1e-15) continue;
ATA[r][c] += N[r] * N[c];
}
ATb[r] = ATb[r] + N[r] * samples[i];
}
}
// Gaussian elimination for each coordinate
std::vector<double> x(n), y(n), z(n);
auto solve_system = [&](std::vector<double>& b) {
auto mat = ATA;
// Forward elimination with partial pivoting
for (int k = 0; k < n; ++k) {
int max_row = k;
double max_val = std::abs(mat[k][k]);
for (int r = k + 1; r < n; ++r) {
if (std::abs(mat[r][k]) > max_val) {
max_val = std::abs(mat[r][k]);
max_row = r;
}
}
if (max_val < 1e-15) continue;
if (max_row != k) {
std::swap(mat[k], mat[max_row]);
std::swap(b[k], b[max_row]);
}
double pivot = mat[k][k];
for (int r = k + 1; r < n; ++r) {
double factor = mat[r][k] / pivot;
for (int c = k; c < n; ++c)
mat[r][c] -= factor * mat[k][c];
b[r] -= factor * b[k];
}
}
// Back substitution
for (int k = n - 1; k >= 0; --k) {
double sum = b[k];
for (int c = k + 1; c < n; ++c)
sum -= mat[k][c] * b[c];
b[k] = (std::abs(mat[k][k]) > 1e-15) ? sum / mat[k][k] : 0.0;
}
};
std::vector<double> bx(n), by(n), bz(n);
for (int i = 0; i < n; ++i) {
bx[i] = ATb[i].x();
by[i] = ATb[i].y();
bz[i] = ATb[i].z();
}
solve_system(bx);
solve_system(by);
solve_system(bz);
std::vector<Point3D> cps(n);
for (int i = 0; i < n; ++i)
cps[i] = Point3D(bx[i], by[i], bz[i]);
// For closed curves, wrap first 'degree' ctrl points
// to ensure C0 continuity at the seam
for (int i = 0; i < degree && i < n; ++i)
cps.push_back(cps[i]);
std::vector<double> weights(cps.size(), 1.0);
return curves::NurbsCurve(cps, make_uniform_knots(static_cast<int>(cps.size()), degree),
weights, degree);
}
// ── Find boundary edges by counting undirected edge occurrences across faces ──
// Since add_face never sets opposite_index, all edges appear alone. Count each
// undirected edge across all faces; those appearing exactly once are boundary edges.
std::vector<std::pair<int,int>> find_boundary_edges(const HalfedgeMesh& mesh) {
std::map<std::pair<int,int>, int> edge_count;
// Also record one face-side direction for each edge so we can orient the boundary
std::map<std::pair<int,int>, std::pair<int,int>> edge_dir; // undirected key -> face-winding direction
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
int n = static_cast<int>(fv.size());
for (int i = 0; i < n; ++i) {
int v0 = fv[i];
int v1 = fv[(i + 1) % n];
auto key = std::make_pair(std::min(v0, v1), std::max(v0, v1));
edge_count[key]++;
if (edge_count[key] == 1)
edge_dir[key] = std::make_pair(v0, v1); // face winding direction
}
}
std::vector<std::pair<int,int>> result;
for (const auto& [key, cnt] : edge_count) {
if (cnt == 1) {
// Boundary edge: the boundary direction is opposite to face winding
auto [fv0, fv1] = edge_dir[key];
result.emplace_back(fv1, fv0); // reverse for boundary loop direction
}
}
return result;
}
// ── Trace boundary loops from adjacency map ──
std::vector<std::vector<Point3D>> trace_boundary_loops(
const HalfedgeMesh& mesh,
const std::vector<std::pair<int,int>>& be)
{
// Build adjacency: vertex -> list of adjacent boundary vertices
std::map<int, std::vector<int>> adj;
for (const auto& [v0, v1] : be) {
adj[v0].push_back(v1);
adj[v1].push_back(v0); // both directions for tracing
}
std::vector<std::vector<Point3D>> loops;
std::set<int> visited_verts;
for (const auto& [start, neighbors] : adj) {
if (visited_verts.count(start)) continue;
if (neighbors.size() != 2) continue; // regular boundary vertex has degree 2 in boundary graph
std::vector<int> loop_verts;
int cur = start;
int prev = -1;
bool closed = false;
while (!visited_verts.count(cur)) {
visited_verts.insert(cur);
loop_verts.push_back(cur);
auto it = adj.find(cur);
if (it == adj.end()) break;
// Find the next unvisited neighbor (not the one we came from)
int next = -1;
for (int nv : it->second) {
if (nv != prev && (!visited_verts.count(nv) || nv == start)) {
next = nv;
break;
}
}
if (next < 0) break;
if (next == start) { closed = true; break; }
prev = cur;
cur = next;
}
if (closed && loop_verts.size() >= 3) {
std::vector<Point3D> pts;
for (int vi : loop_verts)
pts.push_back(mesh.vertex(static_cast<size_t>(vi)));
// Don't add closing duplicate — it would create a singular LS system
loops.push_back(std::move(pts));
}
}
return loops;
}
} // namespace
// ── Public API ────────────────────────────────────────
curves::NurbsCurve mesh_boundary_to_curve(const HalfedgeMesh& mesh) {
auto be = find_boundary_edges(mesh);
if (be.empty()) return curves::NurbsCurve();
auto loops = trace_boundary_loops(mesh, be);
if (loops.empty()) return curves::NurbsCurve();
// Pick the longest loop
size_t best = 0;
for (size_t i = 1; i < loops.size(); ++i)
if (loops[i].size() > loops[best].size())
best = i;
const auto& pts = loops[best];
int n_pts = static_cast<int>(pts.size());
if (n_pts < 3) {
return curves::NurbsCurve();
}
// Upsample if too few points (need at least degree+1 for LS fit)
std::vector<Point3D> sampled;
if (n_pts < 4) {
// Insert midpoints between every pair of vertices
sampled.push_back(pts[0]);
for (int i = 0; i < n_pts; ++i) {
int j = (i + 1) % n_pts;
sampled.push_back((pts[i] + pts[j]) * 0.5);
sampled.push_back(pts[j]);
}
} else if (n_pts <= 64) {
sampled = pts;
} else {
int step = n_pts / 64;
for (int i = 0; i < 64; ++i)
sampled.push_back(pts[i * step]);
sampled.push_back(pts[0]); // close
}
auto params = chord_length_params(sampled);
// For a closed loop, degree 3 with ctrl_pts = max(4, sampled/2)
int num_ctrl = std::max(4, static_cast<int>(sampled.size()) / 2);
return ls_fit_bspline(sampled, params, 3, num_ctrl);
}
std::vector<curves::NurbsCurve> extract_profiles_from_mesh(
const HalfedgeMesh& mesh, const Vector3D& projection_axis)
{
auto be = find_boundary_edges(mesh);
if (be.empty()) return {};
auto loops = trace_boundary_loops(mesh, be);
std::vector<curves::NurbsCurve> results;
// Normalize projection axis
Vector3D axis = projection_axis.normalized();
for (auto& pts : loops) {
if (pts.size() < 4) continue;
// Project points onto the plane perpendicular to axis
std::vector<Point3D> projected;
for (auto& p : pts) {
double d = p.dot(axis);
projected.push_back(p - d * axis);
}
// Subsample if needed
std::vector<Point3D> sampled;
int n_pts = static_cast<int>(projected.size());
if (n_pts <= 64) {
sampled = projected;
} else {
int step = n_pts / 64;
for (int i = 0; i < 64; ++i)
sampled.push_back(projected[i * step]);
sampled.push_back(projected[0]);
}
auto params = chord_length_params(sampled);
int num_ctrl = std::max(4, static_cast<int>(sampled.size()) / 2);
results.push_back(ls_fit_bspline(sampled, params, 3, num_ctrl));
}
return results;
}
std::vector<curves::NurbsCurve> mesh_contour_to_curves(
const HalfedgeMesh& mesh, double z_height)
{
// Collect all edge intersections with z=z_height by iterating faces
// Store unique intersection points keyed by their position
struct ContourPoint {
Point3D pt;
int face_idx;
int vertex_a; // edge endpoint vertex indices (for dedup)
int vertex_b;
};
struct PtKey {
double x, y, z;
bool operator<(const PtKey& o) const {
if (std::abs(x - o.x) > 1e-9) return x < o.x;
if (std::abs(y - o.y) > 1e-9) return y < o.y;
if (std::abs(z - o.z) > 1e-9) return z < o.z;
return false;
}
};
std::map<PtKey, ContourPoint> unique_pts;
std::map<int, std::vector<PtKey>> face_to_pts; // face_idx -> contour points in this face
// Iterate faces, find edges crossing z=z_height
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
int n = static_cast<int>(fv.size());
for (int i = 0; i < n; ++i) {
int v0 = fv[i];
int v1 = fv[(i + 1) % n];
double z0 = mesh.vertex(static_cast<size_t>(v0)).z();
double z1 = mesh.vertex(static_cast<size_t>(v1)).z();
if ((z0 <= z_height && z1 >= z_height) ||
(z0 >= z_height && z1 <= z_height)) {
if (std::abs(z1 - z0) < 1e-15) continue;
double t = (z_height - z0) / (z1 - z0);
Point3D pt = mesh.vertex(static_cast<size_t>(v0)) +
t * (mesh.vertex(static_cast<size_t>(v1)) -
mesh.vertex(static_cast<size_t>(v0)));
PtKey key{pt.x(), pt.y(), pt.z()};
if (unique_pts.find(key) == unique_pts.end()) {
unique_pts[key] = {pt, static_cast<int>(fi), v0, v1};
}
face_to_pts[static_cast<int>(fi)].push_back(key);
}
}
}
if (unique_pts.empty()) return {};
// Build adjacency: connect intersection points within each face
struct PtCmp {
bool operator()(const Point3D& a, const Point3D& b) const {
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();
}
};
std::map<Point3D, std::vector<Point3D>, PtCmp> pt_adj;
for (auto& [fi, keys] : face_to_pts) {
if (keys.size() < 2) continue;
// Connect consecutive points in this face
for (size_t i = 0; i + 1 < keys.size(); ++i) {
auto& p0 = unique_pts[keys[i]].pt;
auto& p1 = unique_pts[keys[i+1]].pt;
pt_adj[p0].push_back(p1);
pt_adj[p1].push_back(p0);
}
if (keys.size() > 2) {
auto& p0 = unique_pts[keys[0]].pt;
auto& pn = unique_pts[keys.back()].pt;
pt_adj[p0].push_back(pn);
pt_adj[pn].push_back(p0);
}
}
// Now trace connected components using the adjacency
std::set<Point3D, PtCmp> visited;
std::vector<std::vector<Point3D>> components;
for (auto& [pt, neighbors] : pt_adj) {
if (visited.count(pt)) continue;
if (neighbors.empty()) continue;
std::vector<Point3D> comp;
Point3D cur = pt;
Point3D prev_pt = pt; // arbitrary initial
while (!visited.count(cur)) {
visited.insert(cur);
comp.push_back(cur);
auto it = pt_adj.find(cur);
if (it == pt_adj.end()) break;
// Find an unvisited neighbor (not the one we came from)
Point3D next;
bool found = false;
for (auto& n : it->second) {
// Skip if it's the previous point (unless we're wrapping around)
if ((n - prev_pt).norm() < 1e-9) continue;
if (!visited.count(n)) {
next = n;
found = true;
break;
}
// Wrap: if we're back at the start and this is the start point
if (comp.size() > 1 && (n - comp[0]).norm() < 1e-9) {
next = n;
found = true;
break;
}
}
if (!found) break;
prev_pt = cur;
cur = next;
}
if (comp.size() >= 3) {
// Close the loop for fitting
comp.push_back(comp[0]);
components.push_back(std::move(comp));
}
}
std::vector<curves::NurbsCurve> results;
for (auto& comp : components) {
if (comp.size() < 4) continue;
auto sampled = comp;
if (sampled.size() > 64) {
std::vector<Point3D> sub;
int step = static_cast<int>(sampled.size()) / 64;
for (int i = 0; i < 64; ++i)
sub.push_back(sampled[i * step]);
sub.push_back(sampled[0]);
sampled = sub;
}
auto params = chord_length_params(sampled);
int num_ctrl = std::max(4, static_cast<int>(sampled.size()) / 2);
results.push_back(ls_fit_bspline(sampled, params, 3, num_ctrl));
}
return results;
}
} // namespace vde::mesh