feat(core+chart): ColorScale::stops() 暴露断点 + ColorMapService 连续插值色阶服务
- core::ColorScale 新增 stops() 方法,返回升序 (value, Rgba) 断点列表,供连续插值用。 - app::ColorMapService:从 ColorScale 构建,支持 normalized()/colorAtContinuous()/colorAtDiscrete(); 连续模式在归一化断点位置间线性插值 RGB,与原版 Plotly colorscale 一致。 - cmake/qwt.cmake 补加 QWT_MOC_INCLUDE=1,修复 Qwt AUTOMOC self-include 宏保护缺失导致 Q_OBJECT MOC 代码不编入 .obj 的链接错误(LNK2019 x61 系列)。 - 新增 TDD 测试 ColorMapService.*(4 个用例,全绿)。
This commit is contained in:
parent
7808b8422a
commit
c7fec86d3b
|
|
@ -24,6 +24,9 @@ set_target_properties(qwt PROPERTIES AUTOMOC ON)
|
|||
target_include_directories(qwt PUBLIC "${QWT_SRC_DIR}")
|
||||
target_link_libraries(qwt PUBLIC Qt6::Widgets Qt6::Concurrent Qt6::PrintSupport Qt6::Svg)
|
||||
target_compile_features(qwt PUBLIC cxx_std_17)
|
||||
# QWT_MOC_INCLUDE=1:启用 Qwt 源文件末尾 #if QWT_MOC_INCLUDE 保护的 #include "moc_xxx.cpp"。
|
||||
# Qwt 原生用 qmake(qwtbuild.pri 里设置),CMake 构建需显式定义,否则 MOC 元对象代码不被编译进 .obj。
|
||||
target_compile_definitions(qwt PRIVATE QWT_MOC_INCLUDE=1)
|
||||
if(MSVC)
|
||||
target_compile_options(qwt PRIVATE /bigobj /EHsc /wd4244 /wd4267 /wd4305 /wd4456)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
#include "panels/chart/ColorMapService.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
inline double clamp01(double v) {
|
||||
return v < 0.0 ? 0.0 : (v > 1.0 ? 1.0 : v);
|
||||
}
|
||||
inline unsigned char lerpByte(unsigned char a, unsigned char b, double t) {
|
||||
return static_cast<unsigned char>(a + (b - a) * t + 0.5);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ColorMapService::ColorMapService(const core::ColorScale& scale)
|
||||
: scale_(scale) {
|
||||
auto raw = scale.stops();
|
||||
if (raw.empty()) {
|
||||
minVal_ = 0.0;
|
||||
maxVal_ = 1.0;
|
||||
return;
|
||||
}
|
||||
minVal_ = raw.front().first;
|
||||
maxVal_ = raw.back().first;
|
||||
double range = maxVal_ - minVal_;
|
||||
normStops_.reserve(raw.size());
|
||||
for (const auto& [val, color] : raw) {
|
||||
double pos = (range > 0.0) ? (val - minVal_) / range : 0.0;
|
||||
normStops_.push_back({clamp01(pos), color});
|
||||
}
|
||||
}
|
||||
|
||||
double ColorMapService::normalized(double v) const {
|
||||
double range = maxVal_ - minVal_;
|
||||
if (range <= 0.0) return 0.0;
|
||||
return clamp01((v - minVal_) / range);
|
||||
}
|
||||
|
||||
core::Rgba ColorMapService::colorAtContinuous(double v) const {
|
||||
if (normStops_.empty()) return core::Rgba{0, 0, 0, 255};
|
||||
if (normStops_.size() == 1) return normStops_.front().color;
|
||||
|
||||
double t = normalized(v);
|
||||
|
||||
// 找到 t 落在哪两个 normStop 之间
|
||||
if (t <= normStops_.front().pos) return normStops_.front().color;
|
||||
if (t >= normStops_.back().pos) return normStops_.back().color;
|
||||
|
||||
// 二分查找第一个 pos > t
|
||||
auto it = std::upper_bound(normStops_.begin(), normStops_.end(), t,
|
||||
[](double val, const NormStop& s) { return val < s.pos; });
|
||||
|
||||
const NormStop& hi = *it;
|
||||
const NormStop& lo = *(it - 1);
|
||||
double segLen = hi.pos - lo.pos;
|
||||
double frac = (segLen > 0.0) ? (t - lo.pos) / segLen : 0.0;
|
||||
|
||||
return core::Rgba{
|
||||
lerpByte(lo.color.r, hi.color.r, frac),
|
||||
lerpByte(lo.color.g, hi.color.g, frac),
|
||||
lerpByte(lo.color.b, hi.color.b, frac),
|
||||
lerpByte(lo.color.a, hi.color.a, frac)
|
||||
};
|
||||
}
|
||||
|
||||
core::Rgba ColorMapService::colorAtDiscrete(double v) const {
|
||||
return scale_.colorAt(v);
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
#include "model/ColorScale.hpp"
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 连续/离散双模式色阶服务,供散点渲染(连续插值)和网格/图例(阶梯)使用。
|
||||
// 连续模式:按归一化位置在相邻断点之间线性插值 RGB,与原版 Plotly colorscale 一致。
|
||||
class ColorMapService {
|
||||
public:
|
||||
explicit ColorMapService(const core::ColorScale& scale);
|
||||
|
||||
// 将数据值归一化到 [0,1](min=首断点值, max=末断点值),超范围 clamp。
|
||||
double normalized(double v) const;
|
||||
|
||||
// 连续插值取色(散点用):按断点位置线性插值 RGB。
|
||||
core::Rgba colorAtContinuous(double v) const;
|
||||
|
||||
// 离散阶梯取色(网格/图例用):复用 ColorScale::colorAt。
|
||||
core::Rgba colorAtDiscrete(double v) const;
|
||||
|
||||
// 返回原始 ColorScale 引用(色阶条图例用)。
|
||||
const core::ColorScale& scale() const { return scale_; }
|
||||
|
||||
private:
|
||||
core::ColorScale scale_;
|
||||
// 归一化位置与颜色预计算缓存(stops 已升序)。
|
||||
struct NormStop {
|
||||
double pos; // normalized position in [0,1]
|
||||
core::Rgba color;
|
||||
};
|
||||
std::vector<NormStop> normStops_;
|
||||
double minVal_;
|
||||
double maxVal_;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -40,6 +40,13 @@ std::vector<double> ColorScale::stopValues() const {
|
|||
return v;
|
||||
}
|
||||
|
||||
std::vector<std::pair<double, Rgba>> ColorScale::stops() const {
|
||||
std::vector<std::pair<double, Rgba>> result;
|
||||
result.reserve(stops_.size());
|
||||
for (const auto& s : stops_) result.emplace_back(s.value, s.color);
|
||||
return result;
|
||||
}
|
||||
|
||||
Rgba ColorScale::colorAt(double value) const {
|
||||
if (std::isnan(value)) return nan_.value_or(Rgba{0, 0, 0, 0});
|
||||
if (stops_.empty()) return Rgba{0, 0, 0, 0};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
namespace geopro::core {
|
||||
|
|
@ -15,6 +16,8 @@ public:
|
|||
void addStop(double value, Rgba color); // 内部保持按 value 升序
|
||||
Rgba colorAt(double value) const; // 含 under/over/NaN 处理
|
||||
std::vector<double> stopValues() const; // 升序分段值(供等值线用真实非均匀级)
|
||||
// 返回升序断点 (value, color) 列表,供连续插值用。
|
||||
std::vector<std::pair<double, Rgba>> stops() const;
|
||||
std::size_t stopCount() const { return stops_.size(); }
|
||||
void setUnder(Rgba c) { under_ = c; }
|
||||
void setOver(Rgba c) { over_ = c; }
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ endif()
|
|||
|
||||
target_include_directories(geopro_tests PRIVATE ${CMAKE_SOURCE_DIR}/src/app)
|
||||
target_sources(geopro_tests PRIVATE app/test_chart_strategy_registry.cpp)
|
||||
# ColorMapService 测试(geopro_desktop 是可执行文件,直接把源加入测试目标)
|
||||
target_sources(geopro_tests PRIVATE
|
||||
app/test_colormap_service.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/app/panels/chart/ColorMapService.cpp
|
||||
)
|
||||
|
||||
# controller 层:DatasetDetailController 编排集成测试(QSignalSpy 验证 chartReady/loadFailed)。
|
||||
find_package(Qt6 COMPONENTS Test REQUIRED)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include "panels/chart/ColorMapService.hpp"
|
||||
using namespace geopro;
|
||||
|
||||
TEST(ColorMapService, ContinuousInterpolatesBetweenStops) {
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||
cs.addStop(100.0, core::Rgba{255, 255, 255, 255});
|
||||
app::ColorMapService svc(cs);
|
||||
EXPECT_NEAR(svc.normalized(50.0), 0.5, 1e-9);
|
||||
auto c = svc.colorAtContinuous(50.0); // 黑白中点应约为灰
|
||||
EXPECT_GT(c.r, 100); EXPECT_LT(c.r, 160);
|
||||
}
|
||||
|
||||
TEST(ColorMapService, NormalizedClampsOutOfRange) {
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||
cs.addStop(100.0, core::Rgba{255, 255, 255, 255});
|
||||
app::ColorMapService svc(cs);
|
||||
EXPECT_NEAR(svc.normalized(-10.0), 0.0, 1e-9);
|
||||
EXPECT_NEAR(svc.normalized(110.0), 1.0, 1e-9);
|
||||
}
|
||||
|
||||
TEST(ColorMapService, ColorAtContinuousAtExtremes) {
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{10, 20, 30, 255});
|
||||
cs.addStop(100.0, core::Rgba{200, 210, 220, 255});
|
||||
app::ColorMapService svc(cs);
|
||||
auto cMin = svc.colorAtContinuous(0.0);
|
||||
EXPECT_EQ(cMin.r, 10);
|
||||
EXPECT_EQ(cMin.g, 20);
|
||||
EXPECT_EQ(cMin.b, 30);
|
||||
auto cMax = svc.colorAtContinuous(100.0);
|
||||
EXPECT_EQ(cMax.r, 200);
|
||||
EXPECT_EQ(cMax.g, 210);
|
||||
EXPECT_EQ(cMax.b, 220);
|
||||
}
|
||||
|
||||
TEST(ColorMapService, ScaleRefReturnsOriginal) {
|
||||
core::ColorScale cs;
|
||||
cs.addStop(0.0, core::Rgba{0, 0, 0, 255});
|
||||
cs.addStop(50.0, core::Rgba{128, 0, 0, 255});
|
||||
cs.addStop(100.0, core::Rgba{255, 0, 0, 255});
|
||||
app::ColorMapService svc(cs);
|
||||
EXPECT_EQ(svc.scale().stopCount(), 3u);
|
||||
}
|
||||
Loading…
Reference in New Issue