renderer: add generic scalar field color path
This commit is contained in:
@@ -149,9 +149,10 @@ float mercatorLatitudeRadians(float worldY) {
|
||||
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
|
||||
}
|
||||
|
||||
vec3 weatherSpeedColor(float speed) {
|
||||
// Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching.
|
||||
float t = clamp(speed / 30.0, 0.0, 1.0);
|
||||
vec3 weatherDefaultValueColor(float value) {
|
||||
// Historical default ramp endpoints are 0 and 30 generic field-value
|
||||
// units. Hosts provide explicit stops whenever another scale is needed.
|
||||
float t = clamp(value / 30.0, 0.0, 1.0);
|
||||
vec3 slow = vec3(0.08, 0.18, 0.65);
|
||||
vec3 medium = vec3(0.08, 0.80, 0.78);
|
||||
vec3 fast = vec3(0.98, 0.22, 0.10);
|
||||
@@ -184,15 +185,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) {
|
||||
length(velocity) > 0.00001;
|
||||
}
|
||||
|
||||
vec4 weatherRampColor(float speed) {
|
||||
vec4 weatherRampColor(float value) {
|
||||
int count = int(u_weather_ramp.x);
|
||||
if (count < 2) return vec4(weatherSpeedColor(speed), 1.0);
|
||||
if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0);
|
||||
vec4 color = u_weather_colors[0];
|
||||
for (int i = 1; i < 8; ++i) {
|
||||
if (i >= count) break;
|
||||
float lo = u_weather_stops[i - 1].x;
|
||||
float hi = u_weather_stops[i].x;
|
||||
if (speed <= hi) return mix(color, u_weather_colors[i], clamp((speed - lo) / (hi - lo), 0.0, 1.0));
|
||||
if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0));
|
||||
color = u_weather_colors[i];
|
||||
}
|
||||
return color;
|
||||
@@ -207,11 +208,20 @@ void main() {
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
if (field0.a < 0.5 || field1.a < 0.5) discard;
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(velocity))) discard;
|
||||
vec4 color = weatherRampColor(length(velocity));
|
||||
float fieldValue;
|
||||
if (u_weather_ramp.y > 0.5) {
|
||||
vec2 vectorValue = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(vectorValue))) discard;
|
||||
fieldValue = length(vectorValue);
|
||||
} else {
|
||||
fieldValue = mix((field0.r * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.r * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (isnan(fieldValue)) discard;
|
||||
}
|
||||
vec4 color = weatherRampColor(fieldValue);
|
||||
float alpha = color.a * u_visual.x;
|
||||
fragColor = vec4(color.rgb * alpha, alpha);
|
||||
return;
|
||||
|
||||
@@ -19,7 +19,8 @@ struct alignas(16) ParticleGpuValidationUBO {
|
||||
// Preferred respawn domain. It is the expanded stable viewport intersected
|
||||
// with fieldBounds, or fieldBounds when that intersection is unusable.
|
||||
std::array<float, 4> respawnBounds;
|
||||
// Per-texture signed velocity decoding scales in canonical m/s.
|
||||
// Per-texture signed vector decoding scales. WeatherColor also uses these
|
||||
// for its explicitly selected generic scalar/value plane.
|
||||
// z is viewportDensityActive for the formal validation path.
|
||||
std::array<float, 4> fieldScale;
|
||||
// Host-owned base colours. They are independent from WeatherColor stops.
|
||||
|
||||
@@ -3,8 +3,14 @@
|
||||
#include <mbgl/style/layer.hpp>
|
||||
#include <mbgl/style/layers/weather_color_stop.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mbgl::style {
|
||||
|
||||
// The renderer must be told which canonical plane produces color values.
|
||||
// Defaulting to VectorMagnitude preserves the v1 Wind color behavior.
|
||||
enum class FieldValueSource : uint8_t { Scalar, VectorMagnitude };
|
||||
|
||||
// A field-speed colour display. It deliberately remains distinct from
|
||||
// RasterParticleLayer: both consume the same canonical field source but own
|
||||
// separate GPU resources and draw passes.
|
||||
@@ -16,6 +22,8 @@ public:
|
||||
void setOpacity(float);
|
||||
const std::vector<WeatherColorStop>& getColorStops() const;
|
||||
bool setColorStops(std::vector<WeatherColorStop>);
|
||||
FieldValueSource getFieldValueSource() const;
|
||||
void setFieldValueSource(FieldValueSource);
|
||||
class Impl;
|
||||
const Impl& impl() const;
|
||||
Mutable<Impl> mutableImpl() const;
|
||||
|
||||
@@ -12,16 +12,18 @@ inline bool validWeatherRGBA(const std::array<float, 4>& color) {
|
||||
return true;
|
||||
}
|
||||
struct WeatherColorStop {
|
||||
float speedMps;
|
||||
// A generic field value. Its unit and meaning come from the explicitly
|
||||
// selected field value source, never from this color ramp.
|
||||
float value;
|
||||
std::array<float, 4> color;
|
||||
bool operator==(const WeatherColorStop& other) const { return speedMps == other.speedMps && color == other.color; }
|
||||
bool operator==(const WeatherColorStop& other) const { return value == other.value && color == other.color; }
|
||||
};
|
||||
constexpr size_t MaxWeatherColorStops = 8;
|
||||
inline bool validWeatherColorStops(const std::vector<WeatherColorStop>& stops) {
|
||||
if (stops.size() < 2 || stops.size() > MaxWeatherColorStops) return false;
|
||||
for (size_t i = 0; i < stops.size(); ++i) {
|
||||
if (!std::isfinite(stops[i].speedMps) || !validWeatherRGBA(stops[i].color) ||
|
||||
(i && stops[i - 1].speedMps >= stops[i].speedMps)) return false;
|
||||
if (!std::isfinite(stops[i].value) || !validWeatherRGBA(stops[i].color) ||
|
||||
(i && stops[i - 1].value >= stops[i].value)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -47,9 +47,10 @@ float mercatorLatitudeRadians(float worldY) {
|
||||
return atan(sinh(3.141592653589793 * (1.0 - 2.0 * worldY)));
|
||||
}
|
||||
|
||||
vec3 weatherSpeedColor(float speed) {
|
||||
// Fixed 0..30 m/s scale: no per-frame or per-dataset auto stretching.
|
||||
float t = clamp(speed / 30.0, 0.0, 1.0);
|
||||
vec3 weatherDefaultValueColor(float value) {
|
||||
// Historical default ramp endpoints are 0 and 30 generic field-value
|
||||
// units. Hosts provide explicit stops whenever another scale is needed.
|
||||
float t = clamp(value / 30.0, 0.0, 1.0);
|
||||
vec3 slow = vec3(0.08, 0.18, 0.65);
|
||||
vec3 medium = vec3(0.08, 0.80, 0.78);
|
||||
vec3 fast = vec3(0.98, 0.22, 0.10);
|
||||
@@ -82,15 +83,15 @@ bool fieldVelocityAt(vec2 position, out vec2 velocity) {
|
||||
length(velocity) > 0.00001;
|
||||
}
|
||||
|
||||
vec4 weatherRampColor(float speed) {
|
||||
vec4 weatherRampColor(float value) {
|
||||
int count = int(u_weather_ramp.x);
|
||||
if (count < 2) return vec4(weatherSpeedColor(speed), 1.0);
|
||||
if (count < 2) return vec4(weatherDefaultValueColor(value), 1.0);
|
||||
vec4 color = u_weather_colors[0];
|
||||
for (int i = 1; i < 8; ++i) {
|
||||
if (i >= count) break;
|
||||
float lo = u_weather_stops[i - 1].x;
|
||||
float hi = u_weather_stops[i].x;
|
||||
if (speed <= hi) return mix(color, u_weather_colors[i], clamp((speed - lo) / (hi - lo), 0.0, 1.0));
|
||||
if (value <= hi) return mix(color, u_weather_colors[i], clamp((value - lo) / (hi - lo), 0.0, 1.0));
|
||||
color = u_weather_colors[i];
|
||||
}
|
||||
return color;
|
||||
@@ -105,11 +106,20 @@ void main() {
|
||||
vec4 field0 = texture(u_field_texture, uv);
|
||||
vec4 field1 = texture(u_next_field_texture, uv);
|
||||
if (field0.a < 0.5 || field1.a < 0.5) discard;
|
||||
vec2 velocity = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(velocity))) discard;
|
||||
vec4 color = weatherRampColor(length(velocity));
|
||||
float fieldValue;
|
||||
if (u_weather_ramp.y > 0.5) {
|
||||
vec2 vectorValue = mix((field0.rg * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.rg * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (any(isnan(vectorValue))) discard;
|
||||
fieldValue = length(vectorValue);
|
||||
} else {
|
||||
fieldValue = mix((field0.r * 2.0 - 1.0) * u_field_scale.x,
|
||||
(field1.r * 2.0 - 1.0) * u_field_scale.y,
|
||||
u_marker.x);
|
||||
if (isnan(fieldValue)) discard;
|
||||
}
|
||||
vec4 color = weatherRampColor(fieldValue);
|
||||
float alpha = color.a * u_visual.x;
|
||||
fragColor = vec4(color.rgb * alpha, alpha);
|
||||
return;
|
||||
|
||||
@@ -1077,10 +1077,17 @@ void RenderRasterParticleLayer::update(gfx::ShaderRegistry& shaders,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Scalar-only is a valid V3 Core snapshot, but this velocity-only
|
||||
// renderer stays fail-closed until the V3-03 texture contract exists.
|
||||
if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector())) {
|
||||
// Particle advection consumes only physical U/V velocity. Scalar-only
|
||||
// fields and direction vectors are unavailable until V3-04 defines their
|
||||
// separate advection contract.
|
||||
if (formalSource && (!sourceField->hasVector() || !sourceNextField->hasVector() ||
|
||||
sourceField->vectorSemantics != particle::VectorSemantics::Velocity ||
|
||||
sourceNextField->vectorSemantics != particle::VectorSemantics::Velocity)) {
|
||||
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||
if (gpuResources) {
|
||||
removeTargets(gpuResources->registeredTargets, changes);
|
||||
gpuResources.reset();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!layerGroup || !isLayerRenderable()) return;
|
||||
|
||||
@@ -35,22 +35,34 @@ std::array<float, 4> bounds(const particle::ParticleFieldSnapshot& field) {
|
||||
static_cast<float>((field.east + 180.0) / 360.0), static_cast<float>(mercatorY(field.south))};
|
||||
}
|
||||
|
||||
float scale(const particle::ParticleFieldSnapshot& field) {
|
||||
bool supportsValueSource(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) {
|
||||
return valueSource == style::FieldValueSource::Scalar ? field.hasScalar() : field.hasVector();
|
||||
}
|
||||
|
||||
float scale(const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource) {
|
||||
float result = 1.0f;
|
||||
for (size_t index = 0; index < field.valid.size(); ++index) if (field.valid[index]) {
|
||||
result = std::max(result, std::abs(field.vector[index * 2]));
|
||||
result = std::max(result, std::abs(field.vector[index * 2 + 1]));
|
||||
if (valueSource == style::FieldValueSource::Scalar) {
|
||||
result = std::max(result, std::abs(field.scalar[index]));
|
||||
} else {
|
||||
result = std::max(result, std::abs(field.vector[index * 2]));
|
||||
result = std::max(result, std::abs(field.vector[index * 2 + 1]));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field, const float fieldScale) {
|
||||
std::shared_ptr<PremultipliedImage> image(const particle::ParticleFieldSnapshot& field,
|
||||
const float fieldScale,
|
||||
style::FieldValueSource valueSource) {
|
||||
auto result = std::make_shared<PremultipliedImage>(Size{field.width, field.height});
|
||||
for (uint32_t y = 0; y < field.height; ++y) for (uint32_t x = 0; x < field.width; ++x) {
|
||||
const auto cell = (static_cast<size_t>(y) * field.width + x) * 2;
|
||||
const auto cell = static_cast<size_t>(y) * field.width + x;
|
||||
const auto pixel = (static_cast<size_t>(y) * field.width + x) * PremultipliedImage::channels;
|
||||
result->data[pixel] = static_cast<uint8_t>(std::clamp(field.vector[cell] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(field.vector[cell + 1] / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
const float first = valueSource == style::FieldValueSource::Scalar ? field.scalar[cell] : field.vector[cell * 2];
|
||||
const float second = valueSource == style::FieldValueSource::Scalar ? 0.0f : field.vector[cell * 2 + 1];
|
||||
result->data[pixel] = static_cast<uint8_t>(std::clamp(first / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 1] = static_cast<uint8_t>(std::clamp(second / fieldScale * .5f + .5f, 0.0f, 1.0f) * 255.0f);
|
||||
result->data[pixel + 2] = 0;
|
||||
result->data[pixel + 3] = field.valid[y * field.width + x] ? 255 : 0;
|
||||
}
|
||||
@@ -69,13 +81,14 @@ struct Resources {
|
||||
gfx::Texture2DPtr texture1;
|
||||
float scale0 = 1.0f;
|
||||
float scale1 = 1.0f;
|
||||
style::FieldValueSource valueSource = style::FieldValueSource::VectorMagnitude;
|
||||
};
|
||||
|
||||
class Tweaker final : public gfx::DrawableTweaker {
|
||||
public:
|
||||
Tweaker(std::shared_ptr<Resources> resources_, float mix_, float opacity_, int16_t wrap_,
|
||||
std::vector<style::WeatherColorStop> stops_)
|
||||
: resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)) {}
|
||||
std::vector<style::WeatherColorStop> stops_, style::FieldValueSource valueSource_)
|
||||
: resources(std::move(resources_)), mix(mix_), opacity(opacity_), wrap(wrap_), stops(std::move(stops_)), valueSource(valueSource_) {}
|
||||
void init(gfx::Drawable&) override {}
|
||||
void execute(gfx::Drawable& drawable, PaintParameters& parameters) override {
|
||||
drawable.setTexture(resources->texture0, shaders::idParticleGpuValidationPreviousTexture);
|
||||
@@ -94,8 +107,9 @@ public:
|
||||
.matrix = util::cast<float>(parameters.matrixForTile({wrap, {0, 0, 0}})),
|
||||
};
|
||||
ubo.weatherRamp[0] = static_cast<float>(stops.size());
|
||||
ubo.weatherRamp[1] = valueSource == style::FieldValueSource::VectorMagnitude ? 1.0f : 0.0f;
|
||||
for (size_t i = 0; i < stops.size(); ++i) {
|
||||
ubo.weatherStops[i][0] = stops[i].speedMps;
|
||||
ubo.weatherStops[i][0] = stops[i].value;
|
||||
ubo.weatherColors[i] = stops[i].color;
|
||||
}
|
||||
drawable.mutableUniformBuffers().createOrUpdate(shaders::idParticleGpuValidationUBO, &ubo, parameters.context);
|
||||
@@ -106,16 +120,17 @@ private:
|
||||
float opacity;
|
||||
int16_t wrap;
|
||||
std::vector<style::WeatherColorStop> stops;
|
||||
style::FieldValueSource valueSource;
|
||||
};
|
||||
|
||||
bool same(const particle::ParticleFieldSnapshot& a, const particle::ParticleFieldSnapshot& b) {
|
||||
return particle::sameFieldGpuContent(a, b);
|
||||
}
|
||||
|
||||
bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field,
|
||||
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
|
||||
outputScale = scale(field);
|
||||
outputImage = image(field, outputScale);
|
||||
bool uploadField(gfx::Context& context, const particle::ParticleFieldSnapshot& field, style::FieldValueSource valueSource,
|
||||
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
|
||||
outputScale = scale(field, valueSource);
|
||||
outputImage = image(field, outputScale, valueSource);
|
||||
outputTexture = context.createTexture2D();
|
||||
if (!outputTexture) return false;
|
||||
outputTexture->setImage(outputImage);
|
||||
@@ -173,9 +188,8 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
|
||||
resources.reset();
|
||||
return;
|
||||
}
|
||||
// V3-02 accepts scalar snapshots into the Source timeline, but the
|
||||
// existing velocity-only renderer must remain unavailable until V3-03.
|
||||
if (!sourceField->hasVector() || !sourceNextField->hasVector()) {
|
||||
const auto valueSource = static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).valueSource;
|
||||
if (!supportsValueSource(*sourceField, valueSource) || !supportsValueSource(*sourceNextField, valueSource)) {
|
||||
if (layerGroup) static_cast<LayerGroup&>(*layerGroup).clearDrawables();
|
||||
resources.reset();
|
||||
return;
|
||||
@@ -195,23 +209,27 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
|
||||
state = std::make_shared<Resources>(); state->shader = context.getGenericShader(shaders, ShaderName);
|
||||
state->vertices = std::make_shared<gfx::VertexVector<HeatmapTextureLayoutVertex>>(RenderStaticData::heatmapTextureVertices());
|
||||
state->segments = RenderStaticData::heatmapTextureSegments();
|
||||
if (!state->shader || !uploadField(context, *sourceField, state->image0, state->texture0, state->scale0) ||
|
||||
!uploadField(context, *sourceNextField, state->image1, state->texture1, state->scale1)) return;
|
||||
state->valueSource = valueSource;
|
||||
if (!state->shader || !uploadField(context, *sourceField, valueSource, state->image0, state->texture0, state->scale0) ||
|
||||
!uploadField(context, *sourceNextField, valueSource, state->image1, state->texture1, state->scale1)) return;
|
||||
state->t0 = *sourceField; state->t1 = *sourceNextField; resources = state;
|
||||
Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID +
|
||||
" T1=" + state->t1.fieldID + " scale=0..30mps");
|
||||
} else if (!same(state->t0, *sourceField) || !same(state->t1, *sourceNextField)) {
|
||||
" T1=" + state->t1.fieldID + " valueSource=" +
|
||||
(valueSource == style::FieldValueSource::Scalar ? "SCALAR" : "VECTOR_MAGNITUDE"));
|
||||
} else if (state->valueSource != valueSource || !same(state->t0, *sourceField) || !same(state->t1, *sourceNextField)) {
|
||||
const auto old0 = state->t0; const auto old1 = state->t1;
|
||||
auto install = [&](const particle::ParticleFieldSnapshot& wanted, particle::ParticleFieldSnapshot& assigned,
|
||||
std::shared_ptr<PremultipliedImage>& outputImage, gfx::Texture2DPtr& outputTexture, float& outputScale) {
|
||||
if (same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; return true; }
|
||||
if (same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; }
|
||||
assigned = wanted; return uploadField(context, assigned, outputImage, outputTexture, outputScale);
|
||||
// The decoded bundle may be identical while its selected GPU
|
||||
// representation changes (scalar versus vector magnitude).
|
||||
if (state->valueSource == valueSource && same(old0, wanted)) { assigned = wanted; outputImage = state->image0; outputTexture = state->texture0; outputScale = state->scale0; return true; }
|
||||
if (state->valueSource == valueSource && same(old1, wanted)) { assigned = wanted; outputImage = state->image1; outputTexture = state->texture1; outputScale = state->scale1; return true; }
|
||||
assigned = wanted; return uploadField(context, assigned, valueSource, outputImage, outputTexture, outputScale);
|
||||
};
|
||||
particle::ParticleFieldSnapshot t0, t1; std::shared_ptr<PremultipliedImage> i0, i1; gfx::Texture2DPtr x0, x1; float s0 = 1, s1 = 1;
|
||||
if (!install(*sourceField, t0, i0, x0, s0) || !install(*sourceNextField, t1, i1, x1, s1)) return;
|
||||
state->t0 = std::move(t0); state->t1 = std::move(t1); state->image0 = std::move(i0); state->image1 = std::move(i1);
|
||||
state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1;
|
||||
state->texture0 = std::move(x0); state->texture1 = std::move(x1); state->scale0 = s0; state->scale1 = s1; state->valueSource = valueSource;
|
||||
Log::Info(Event::Render, "WeatherColorLayer: event=WEATHER_COLOR_FIELD_READY T0=" + state->t0.fieldID + " T1=" + state->t1.fieldID);
|
||||
}
|
||||
const float mix = sourceTimeline ? timelineFrame.mix : 0.0f;
|
||||
@@ -232,7 +250,7 @@ void RenderWeatherColorLayer::update(gfx::ShaderRegistry& shaders, gfx::Context&
|
||||
builder->setTexture(state->texture0, shaders::idParticleGpuValidationFieldTexture);
|
||||
builder->setTexture(state->texture1, shaders::idParticleGpuValidationNextFieldTexture);
|
||||
builder->addTweaker(std::make_shared<Tweaker>(state, mix, opacity, wrap,
|
||||
static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops)); builder->flush(context);
|
||||
static_cast<const style::WeatherColorLayer::Impl&>(*baseImpl).colorStops, valueSource)); builder->flush(context);
|
||||
auto drawables = builder->clearDrawables(); if (drawables.size() == 1 && drawables.front()) group.addDrawable(std::move(drawables.front()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,14 @@ bool WeatherColorLayer::setColorStops(std::vector<WeatherColorStop> stops) {
|
||||
observer->onLayerChanged(*this);
|
||||
return true;
|
||||
}
|
||||
FieldValueSource WeatherColorLayer::getFieldValueSource() const { return impl().valueSource; }
|
||||
void WeatherColorLayer::setFieldValueSource(FieldValueSource valueSource) {
|
||||
if (valueSource == getFieldValueSource()) return;
|
||||
auto next = mutableImpl();
|
||||
next->valueSource = valueSource;
|
||||
baseImpl = std::move(next);
|
||||
observer->onLayerChanged(*this);
|
||||
}
|
||||
std::optional<conversion::Error> WeatherColorLayer::setPropertyInternal(const std::string& name,
|
||||
const conversion::Convertible& value) {
|
||||
if (name != OpacityProperty) return conversion::Error{"unknown weather-color property: " + name};
|
||||
|
||||
@@ -15,6 +15,7 @@ public:
|
||||
float opacity = 0.62f;
|
||||
// Empty preserves the historical shader palette until a Host supplies stops.
|
||||
std::vector<WeatherColorStop> colorStops;
|
||||
FieldValueSource valueSource = FieldValueSource::VectorMagnitude;
|
||||
DECLARE_LAYER_TYPE_INFO;
|
||||
};
|
||||
|
||||
|
||||
@@ -181,6 +181,9 @@ TEST(Layer, RasterParticleVisualProperties) {
|
||||
TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
|
||||
WeatherColorLayer layer{"weather-color", "raster-array"};
|
||||
ASSERT_STREQ("weather-color", layer.getTypeInfo()->type);
|
||||
EXPECT_EQ(FieldValueSource::VectorMagnitude, layer.getFieldValueSource());
|
||||
layer.setFieldValueSource(FieldValueSource::Scalar);
|
||||
EXPECT_EQ(FieldValueSource::Scalar, layer.getFieldValueSource());
|
||||
EXPECT_FLOAT_EQ(0.62f, layer.getOpacity());
|
||||
layer.setOpacity(0.35f);
|
||||
EXPECT_FLOAT_EQ(0.35f, layer.getOpacity());
|
||||
@@ -192,6 +195,7 @@ TEST(Layer, WeatherColorUsesIndependentFixedOpacity) {
|
||||
EXPECT_EQ(layer.getColorStops(), valid);
|
||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}}));
|
||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {0.0f, {1, 0, 0, 1}}}));
|
||||
EXPECT_TRUE(layer.setColorStops({{-2.0f, {0, 0, 1, 1}}, {3.0f, {1, 0, 0, 1}}}));
|
||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {10.0f, {std::numeric_limits<float>::quiet_NaN(), 0, 0, 1}}}));
|
||||
EXPECT_FALSE(layer.setColorStops({{0.0f, {0, 0, 1, 1}}, {1.0f, {0, 0, 1, 1}}, {2.0f, {0, 0, 1, 1}},
|
||||
{3.0f, {0, 0, 1, 1}}, {4.0f, {0, 0, 1, 1}}, {5.0f, {0, 0, 1, 1}},
|
||||
|
||||
Reference in New Issue
Block a user