diff --git a/src/render/ContourBands.cpp b/src/render/ContourBands.cpp index 817535e..894ed34 100644 --- a/src/render/ContourBands.cpp +++ b/src/render/ContourBands.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -10,7 +11,7 @@ #include #include #include -#include +#include #include @@ -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 toStructuredGrid(const Grid& g) { +// 把含任一 NaN 顶点的网格单元(quad)从网格中剔除→凸包裁剪。 +vtkSmartPointer toCellGrid(const Grid& g) { const int nx = g.nx(), ny = g.ny(); - auto sg = vtkSmartPointer::New(); - sg->SetDimensions(nx, ny, 1); + auto ug = vtkSmartPointer::New(); vtkNew pts; pts->SetNumberOfPoints(static_cast(nx) * ny); vtkNew sc; sc->SetName("v"); sc->SetNumberOfTuples(static_cast(nx) * ny); - for (int j = 0; j < ny; ++j) - for (int i = 0; i < nx; ++i) { - const vtkIdType id = static_cast(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; + for (int j = 0; j < ny; ++j) for (int i = 0; i < nx; ++i) { + const vtkIdType id = static_cast(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); + } + ug->SetPoints(pts); + ug->GetPointData()->SetScalars(sc); + auto idAt = [nx](int i, int j) { return static_cast(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& pts, double tol) { + if (tol <= 0 || pts.size() < 3) return; + std::vector keep(pts.size(), 0); keep.front() = keep.back() = 1; + std::vector> 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 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 surf; surf->SetInputData(sg); + vtkNew surf; surf->SetInputData(toCellGrid(work)); vtkNew banded; banded->SetInputConnection(surf->GetOutputPort()); banded->SetNumberOfContours(static_cast(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)); } } diff --git a/tests/render/test_contour_bands.cpp b/tests/render/test_contour_bands.cpp index 7994241..c4b7b48 100644 --- a/tests/render/test_contour_bands.cpp +++ b/tests/render/test_contour_bands.cpp @@ -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);