feat(core): 阶梯色阶 colorAt+颜色解析(alpha 量纲按来源)

This commit is contained in:
gaozheng 2026-06-07 19:49:53 +08:00
parent fb0586b6e0
commit 868c49ca2c
5 changed files with 104 additions and 0 deletions

View File

@ -2,6 +2,7 @@ find_package(Eigen3 CONFIG REQUIRED)
add_library(geopro_core STATIC add_library(geopro_core STATIC
geo/LocalFrame.cpp geo/LocalFrame.cpp
model/ColorScale.cpp
) )
target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(geopro_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -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

View File

@ -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

View File

@ -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_local_frame.cpp)
target_sources(geopro_tests PRIVATE core/test_model.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) target_link_libraries(geopro_tests PRIVATE geopro_core)
add_subdirectory(spike) # spike S3: banded contour add_subdirectory(spike) # spike S3: banded contour

View File

@ -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
}