feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
2 changed files with 60 additions and 19 deletions
Showing only changes of commit 00c42f7a8d - Show all commits

View File

@ -2,6 +2,7 @@
#include <vtkBandedPolyDataContourFilter.h>
#include <vtkCell.h>
#include <vtkCellArray.h>
#include <vtkCellData.h>
#include <vtkDataSetSurfaceFilter.h>
#include <vtkDoubleArray.h>
@ -10,7 +11,7 @@
#include <vtkPointData.h>
#include <vtkPoints.h>
#include <vtkPolyData.h>
#include <vtkStructuredGrid.h>
#include <vtkUnstructuredGrid.h>
#include <cmath>
@ -70,26 +71,49 @@ Grid smoothGrid(const Grid& g, double strength) {
return out;
}
// 用 Grid 构 vtkStructuredGrid点 (x[i], y[j], 0)NaN 值置 0 但记录于掩膜——
// 首版裁剪在 Task 1.4 接入,这里先全量)。
vtkSmartPointer<vtkStructuredGrid> toStructuredGrid(const Grid& g) {
// 把含任一 NaN 顶点的网格单元quad从网格中剔除→凸包裁剪。
vtkSmartPointer<vtkUnstructuredGrid> toCellGrid(const Grid& g) {
const int nx = g.nx(), ny = g.ny();
auto sg = vtkSmartPointer<vtkStructuredGrid>::New();
sg->SetDimensions(nx, ny, 1);
auto ug = vtkSmartPointer<vtkUnstructuredGrid>::New();
vtkNew<vtkPoints> pts; pts->SetNumberOfPoints(static_cast<vtkIdType>(nx) * ny);
vtkNew<vtkDoubleArray> sc; sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(nx) * ny);
for (int j = 0; j < ny; ++j)
for (int i = 0; i < nx; ++i) {
for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) {
const vtkIdType id = static_cast<vtkIdType>(j) * nx + i;
pts->SetPoint(id, g.x[i], g.y[j], 0.0);
const double v = g.valueAt(i, j);
sc->SetValue(id, std::isnan(v) ? 0.0 : v);
}
sg->SetPoints(pts);
sg->GetPointData()->SetScalars(sc);
return sg;
ug->SetPoints(pts);
ug->GetPointData()->SetScalars(sc);
auto idAt = [nx](int i, int j) { return static_cast<vtkIdType>(j) * nx + i; };
for (int j = 0; j < ny - 1; ++j) for (int i = 0; i < nx - 1; ++i) {
if (!g.hasValue(i, j) || !g.hasValue(i+1, j) || !g.hasValue(i, j+1) || !g.hasValue(i+1, j+1))
continue; // 含 NaN 的 quad 不入网格 → 凸包裁剪
vtkIdType quad[4] = { idAt(i,j), idAt(i+1,j), idAt(i+1,j+1), idAt(i,j+1) };
ug->InsertNextCell(VTK_QUAD, 4, quad);
}
return ug;
}
// Douglas-Peucker 等值线简化(就地)。
void simplifyInPlace(std::vector<Vec2>& pts, double tol) {
if (tol <= 0 || pts.size() < 3) return;
std::vector<char> keep(pts.size(), 0); keep.front() = keep.back() = 1;
std::vector<std::pair<int,int>> st{{0, int(pts.size()) - 1}};
auto dist = [](const Vec2& p, const Vec2& a, const Vec2& b) {
double dx = b.x - a.x, dy = b.y - a.y, L = std::hypot(dx, dy);
if (L < 1e-12) return std::hypot(p.x - a.x, p.y - a.y);
return std::fabs((p.x - a.x) * dy - (p.y - a.y) * dx) / L; };
while (!st.empty()) { auto [s, e] = st.back(); st.pop_back();
double dmax = 0; int idx = -1;
for (int k = s + 1; k < e; ++k) { double d = dist(pts[k], pts[s], pts[e]);
if (d > dmax) { dmax = d; idx = k; } }
if (idx >= 0 && dmax > tol) { keep[idx] = 1; st.push_back({s, idx}); st.push_back({idx, e}); } }
std::vector<Vec2> r; for (size_t k = 0; k < pts.size(); ++k) if (keep[k]) r.push_back(pts[k]);
pts.swap(r);
}
} // namespace
ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const ContourOptions& opt) {
@ -101,8 +125,7 @@ ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const
if (stops.size() < 2) return out;
Grid work = smoothGrid(upsampleBilinear(g, std::max(1, opt.upsample)), opt.smooth);
auto sg = toStructuredGrid(work);
vtkNew<vtkDataSetSurfaceFilter> surf; surf->SetInputData(sg);
vtkNew<vtkDataSetSurfaceFilter> surf; surf->SetInputData(toCellGrid(work));
vtkNew<vtkBandedPolyDataContourFilter> banded;
banded->SetInputConnection(surf->GetOutputPort());
banded->SetNumberOfContours(static_cast<int>(stops.size()));
@ -129,7 +152,7 @@ ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const
out.bands.push_back(std::move(bp));
}
// port1等值线polylines
// port1等值线polylines+ DP 简化
vtkPolyData* edges = banded->GetContourEdgesOutput();
if (edges) {
edges->BuildCells();
@ -142,6 +165,7 @@ ContourBandsResult buildContourBands(const Grid& g, const ColorScale& cs, const
double xyz[3]; cp->GetPoint(p, xyz);
cl.pts.push_back(Vec2{xyz[0], xyz[1]});
}
simplifyInPlace(cl.pts, opt.simplifyTol);
if (cl.pts.size() >= 2) out.lines.push_back(std::move(cl));
}
}

View File

@ -19,6 +19,23 @@ TEST(ContourBands, UpsampleIncreasesDetail) {
EXPECT_GT(rb.bands.size(), ra.bands.size());
}
// 含 NaN 无效角的网格:裁剪后该角不应被任何色带覆盖(所有多边形顶点远离该角)。
TEST(ContourBands, ClipsNaNRegion) {
Grid g(3, 3);
g.x = {0, 1, 2}; g.y = {0, 1, 2};
for (int j = 0; j < 3; ++j) for (int i = 0; i < 3; ++i) g.valueAt(i, j) = 5.0;
g.valueAt(2, 2) = std::nan(""); // 右上角无效
g.vmin = 0; g.vmax = 10;
ColorScale cs; cs.addStop(0, Rgba{0,0,255,255}); cs.addStop(10, Rgba{255,0,0,255});
ContourOptions opt; opt.upsample = 1; opt.smooth = 0; opt.simplifyTol = 0;
auto r = buildContourBands(g, cs, opt);
bool coversCorner = false;
for (const auto& b : r.bands)
for (const auto& p : b.ring)
if (p.x > 1.5 && p.y > 1.5) coversCorner = true;
EXPECT_FALSE(coversCorner);
}
// 2x2 平滑梯度网格 + 2 段色阶 → 至少 1 个色带多边形,多边形顶点 >=3。
TEST(ContourBands, ProducesNonEmptyBands) {
Grid g(3, 3);