feat(core): 阶梯色阶 colorAt+颜色解析(alpha 量纲按来源)
This commit is contained in:
parent
fb0586b6e0
commit
868c49ca2c
|
|
@ -2,6 +2,7 @@ find_package(Eigen3 CONFIG REQUIRED)
|
|||
|
||||
add_library(geopro_core STATIC
|
||||
geo/LocalFrame.cpp
|
||||
model/ColorScale.cpp
|
||||
)
|
||||
|
||||
target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
#define _CRT_SECURE_NO_WARNINGS // MSVC: std::sscanf 解析颜色字符串,长度受限输入安全
|
||||
#include "model/ColorScale.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
namespace geopro::core {
|
||||
|
||||
static unsigned char clampByte(double v) {
|
||||
if (v < 0) v = 0;
|
||||
if (v > 255) v = 255;
|
||||
return static_cast<unsigned char>(v + 0.5);
|
||||
}
|
||||
|
||||
Rgba parseColor(const std::string& s, AlphaScale alpha) {
|
||||
if (!s.empty() && s[0] == '#') {
|
||||
auto hex = [&](int i) {
|
||||
return static_cast<unsigned char>(std::stoi(s.substr(static_cast<size_t>(i), 2), nullptr, 16));
|
||||
};
|
||||
return Rgba{hex(1), hex(3), hex(5), 255};
|
||||
}
|
||||
double r = 0, g = 0, b = 0, a = 1;
|
||||
auto p = s.find('(');
|
||||
if (p != std::string::npos) {
|
||||
std::sscanf(s.c_str() + p, "(%lf,%lf,%lf,%lf", &r, &g, &b, &a);
|
||||
}
|
||||
unsigned char av = (alpha == AlphaScale::Unit) ? clampByte(a * 255.0) : clampByte(a);
|
||||
return Rgba{clampByte(r), clampByte(g), clampByte(b), av};
|
||||
}
|
||||
|
||||
void ColorScale::addStop(double value, Rgba color) {
|
||||
stops_.push_back({value, color});
|
||||
std::sort(stops_.begin(), stops_.end(),
|
||||
[](const Stop& x, const Stop& y) { return x.value < y.value; });
|
||||
}
|
||||
|
||||
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};
|
||||
if (value < stops_.front().value) return under_.value_or(stops_.front().color);
|
||||
if (value >= stops_.back().value) return over_.value_or(stops_.back().color);
|
||||
auto it = std::upper_bound(stops_.begin(), stops_.end(), value,
|
||||
[](double v, const Stop& s) { return v < s.value; });
|
||||
return (it == stops_.begin() ? stops_.front() : *(it - 1)).color;
|
||||
}
|
||||
|
||||
} // namespace geopro::core
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
namespace geopro::core {
|
||||
|
||||
struct Rgba { unsigned char r, g, b, a; };
|
||||
enum class AlphaScale { Unit, Bit255 }; // rgba alpha 量纲:0-1 或 0-255
|
||||
|
||||
Rgba parseColor(const std::string& s, AlphaScale alpha = AlphaScale::Bit255);
|
||||
|
||||
// 阶梯色阶:值落 [stop_i, stop_{i+1}) 取 stop_i 的颜色(与平台一致)。
|
||||
class ColorScale {
|
||||
public:
|
||||
void addStop(double value, Rgba color); // 内部保持按 value 升序
|
||||
Rgba colorAt(double value) const; // 含 under/over/NaN 处理
|
||||
void setUnder(Rgba c) { under_ = c; }
|
||||
void setOver(Rgba c) { over_ = c; }
|
||||
void setNan(Rgba c) { nan_ = c; }
|
||||
bool empty() const { return stops_.empty(); }
|
||||
private:
|
||||
struct Stop { double value; Rgba color; };
|
||||
std::vector<Stop> stops_;
|
||||
std::optional<Rgba> under_, over_, nan_;
|
||||
};
|
||||
|
||||
} // namespace geopro::core
|
||||
|
|
@ -11,6 +11,7 @@ gtest_discover_tests(geopro_tests)
|
|||
|
||||
target_sources(geopro_tests PRIVATE core/test_local_frame.cpp)
|
||||
target_sources(geopro_tests PRIVATE core/test_model.cpp)
|
||||
target_sources(geopro_tests PRIVATE core/test_color_scale.cpp)
|
||||
target_link_libraries(geopro_tests PRIVATE geopro_core)
|
||||
|
||||
add_subdirectory(spike) # spike S3: banded contour 渲染验证
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
#include <gtest/gtest.h>
|
||||
#include "model/ColorScale.hpp"
|
||||
using namespace geopro::core;
|
||||
|
||||
TEST(ColorScale, SteppedLookupTakesLowerStop) {
|
||||
ColorScale cs;
|
||||
cs.addStop(0.0, Rgba{0,0,255,255});
|
||||
cs.addStop(10.0, Rgba{0,255,0,255});
|
||||
cs.addStop(20.0, Rgba{255,0,0,255});
|
||||
EXPECT_EQ(cs.colorAt(5.0).g, 0); // [0,10) -> 蓝
|
||||
EXPECT_EQ(cs.colorAt(5.0).b, 255);
|
||||
EXPECT_EQ(cs.colorAt(15.0).r, 0); // [10,20) -> 绿
|
||||
EXPECT_EQ(cs.colorAt(15.0).g, 255);
|
||||
}
|
||||
|
||||
TEST(ColorScale, UnderOverHandling) {
|
||||
ColorScale cs; cs.addStop(0.0, Rgba{0,0,0,255}); cs.addStop(10.0, Rgba{255,255,255,255});
|
||||
cs.setUnder(Rgba{1,2,3,255}); cs.setOver(Rgba{4,5,6,255});
|
||||
EXPECT_EQ(cs.colorAt(-1.0).r, 1); // under
|
||||
EXPECT_EQ(cs.colorAt(99.0).r, 4); // over
|
||||
}
|
||||
|
||||
TEST(ColorScale, ParseHexAndRgba) {
|
||||
EXPECT_EQ(parseColor("#0000B3").b, 0xB3);
|
||||
auto c255 = parseColor("rgba(0,0,170,255)", AlphaScale::Bit255);
|
||||
EXPECT_EQ(c255.b, 170); EXPECT_EQ(c255.a, 255);
|
||||
auto c1 = parseColor("rgba(0,0,170,1)", AlphaScale::Unit);
|
||||
EXPECT_EQ(c1.b, 170); EXPECT_EQ(c1.a, 255); // 0-1 的 1.0 -> 255
|
||||
}
|
||||
Loading…
Reference in New Issue