feat: add particle visual runtime controls
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled

This commit is contained in:
OpenAI Codex
2026-09-03 19:47:47 +08:00
parent eae8652486
commit e48fa087e0
17 changed files with 704 additions and 54 deletions

View File

@@ -1117,7 +1117,10 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
auto* layer = map->getStyle().getLayer(jni::Make<std::string>(env, layerId));
if (!layer) return false;
const auto property = jni::Make<std::string>(env, propertyName);
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count") {
if (property != "raster-particle-time-mix" && property != "raster-particle-particle-count" &&
property != "raster-particle-line-width" && property != "raster-particle-particle-length" &&
property != "raster-particle-particle-opacity" && property != "raster-particle-trail-opacity" &&
property != "raster-particle-trail-fade" && property != "raster-particle-speed-color-enabled") {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_PROPERTY");
return false;
}
@@ -1126,10 +1129,14 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=UNSUPPORTED_LAYER");
return false;
}
if (!std::isfinite(value)) {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_FLOAT");
return false;
}
if (property == "raster-particle-time-mix") {
particleLayer->setTimeMix(value);
} else {
if (!std::isfinite(value) || value < 0.0f || std::floor(value) != value) {
} else if (property == "raster-particle-particle-count") {
if (value < 0.0f || std::floor(value) != value) {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
return false;
}
@@ -1139,6 +1146,22 @@ jni::jboolean NativeMapView::setStyleLayerFloatProperty(JNIEnv& env,
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_PARTICLE_COUNT");
return false;
}
} else if (property == "raster-particle-line-width") {
particleLayer->setLineWidth(value);
} else if (property == "raster-particle-particle-length") {
particleLayer->setParticleLength(value);
} else if (property == "raster-particle-particle-opacity") {
particleLayer->setParticleOpacity(value);
} else if (property == "raster-particle-trail-opacity") {
particleLayer->setTrailOpacity(value);
} else if (property == "raster-particle-trail-fade") {
particleLayer->setTrailFade(value);
} else {
if (value != 0.0f && value != 1.0f) {
mbgl::Log::Error(mbgl::Event::JNI, "ParticleGPU: event=STYLE_PROPERTY_SET result=INVALID_COLOR_MODE");
return false;
}
particleLayer->setSpeedColorEnabled(value > 0.5f);
}
mbgl::Log::Info(mbgl::Event::JNI,
"ParticleGPU: event=STYLE_PROPERTY_SET layerId=" + jni::Make<std::string>(env, layerId) +

View File

@@ -24,6 +24,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private var fixtureMode = FIXTURE_UNIFORM_EAST
private var timeMix = 0.0f
private var particleCount = PARTICLE_COUNT_256
private var lineWidth = DEFAULT_LINE_WIDTH
private var particleLength = DEFAULT_PARTICLE_LENGTH
private var particleOpacity = DEFAULT_PARTICLE_OPACITY
private var trailOpacity = DEFAULT_TRAIL_OPACITY
private var trailFade = DEFAULT_TRAIL_FADE
private var speedColorEnabled = true
private var timeWindowMode = false
private var styleLoadEpoch = 0L
private var styleReady = false
@@ -35,6 +41,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
fixtureMode = savedInstanceState?.getInt(STATE_FIXTURE_MODE, FIXTURE_UNIFORM_EAST) ?: FIXTURE_UNIFORM_EAST
timeMix = savedInstanceState?.getFloat(STATE_TIME_MIX, 0.0f) ?: 0.0f
particleCount = savedInstanceState?.getInt(STATE_PARTICLE_COUNT, PARTICLE_COUNT_256) ?: PARTICLE_COUNT_256
lineWidth = savedInstanceState?.getFloat(STATE_LINE_WIDTH, DEFAULT_LINE_WIDTH) ?: DEFAULT_LINE_WIDTH
particleLength = savedInstanceState?.getFloat(STATE_PARTICLE_LENGTH, DEFAULT_PARTICLE_LENGTH) ?: DEFAULT_PARTICLE_LENGTH
particleOpacity = savedInstanceState?.getFloat(STATE_PARTICLE_OPACITY, DEFAULT_PARTICLE_OPACITY) ?: DEFAULT_PARTICLE_OPACITY
trailOpacity = savedInstanceState?.getFloat(STATE_TRAIL_OPACITY, DEFAULT_TRAIL_OPACITY) ?: DEFAULT_TRAIL_OPACITY
trailFade = savedInstanceState?.getFloat(STATE_TRAIL_FADE, DEFAULT_TRAIL_FADE) ?: DEFAULT_TRAIL_FADE
speedColorEnabled = savedInstanceState?.getBoolean(STATE_SPEED_COLOR, true) ?: true
timeWindowMode = savedInstanceState?.getBoolean(STATE_TIME_WINDOW, false) ?: false
super.onCreate(savedInstanceState)
Log.i(
@@ -89,6 +101,11 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
fixtureMode = FIXTURE_UNIFORM_NORTH
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "UNIFORM_NORTH")
}
findViewById<Button>(R.id.validationVortex).setOnClickListener {
timeWindowMode = false
fixtureMode = FIXTURE_VORTEX
if (this::maplibreMap.isInitialized) loadValidationStyle(active, enabled, "VORTEX")
}
findViewById<Button>(R.id.validationTimeWindow).setOnClickListener {
timeWindowMode = true
fixtureMode = FIXTURE_UNIFORM_EAST
@@ -103,6 +120,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
findViewById<Button>(R.id.validationParticle4096).setOnClickListener { setParticleCount(PARTICLE_COUNT_4096) }
findViewById<Button>(R.id.validationParticle16384).setOnClickListener { setParticleCount(PARTICLE_COUNT_16384) }
findViewById<Button>(R.id.validationParticle65536).setOnClickListener { setParticleCount(PARTICLE_COUNT_65536) }
findViewById<Button>(R.id.validationParticle262144).setOnClickListener { setParticleCount(PARTICLE_COUNT_262144) }
findViewById<Button>(R.id.validationParticle1048576).setOnClickListener { setParticleCount(PARTICLE_COUNT_1048576) }
findViewById<Button>(R.id.validationAdvanceWindow).setOnClickListener {
if (this::maplibreMap.isInitialized) advanceRuntimeFieldWindow()
@@ -110,6 +128,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
findViewById<Button>(R.id.validationDuplicateWindow).setOnClickListener {
if (this::maplibreMap.isInitialized) duplicateRuntimeFieldWindow()
}
findViewById<Button>(R.id.validationVisualDefault).setOnClickListener { resetVisualDefaults() }
findViewById<Button>(R.id.validationVisualSizeDown).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth - 0.5f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
findViewById<Button>(R.id.validationVisualSizeUp).setOnClickListener { setVisualFloat("raster-particle-line-width", lineWidth + 0.5f) { lineWidth = it.coerceIn(1.0f, 8.0f) } }
findViewById<Button>(R.id.validationVisualLengthDown).setOnClickListener { setVisualFloat("raster-particle-particle-length", particleLength - 0.004f) { particleLength = it.coerceIn(0.002f, 0.080f) } }
findViewById<Button>(R.id.validationVisualLengthUp).setOnClickListener { setVisualFloat("raster-particle-particle-length", particleLength + 0.004f) { particleLength = it.coerceIn(0.002f, 0.080f) } }
findViewById<Button>(R.id.validationVisualOpacityDown).setOnClickListener { setVisualFloat("raster-particle-particle-opacity", particleOpacity - 0.1f) { particleOpacity = it.coerceIn(0.0f, 1.0f) } }
findViewById<Button>(R.id.validationVisualOpacityUp).setOnClickListener { setVisualFloat("raster-particle-particle-opacity", particleOpacity + 0.1f) { particleOpacity = it.coerceIn(0.0f, 1.0f) } }
findViewById<Button>(R.id.validationVisualTrailDown).setOnClickListener { setVisualFloat("raster-particle-trail-opacity", trailOpacity - 0.1f) { trailOpacity = it.coerceIn(0.0f, 1.0f) } }
findViewById<Button>(R.id.validationVisualTrailUp).setOnClickListener { setVisualFloat("raster-particle-trail-opacity", trailOpacity + 0.1f) { trailOpacity = it.coerceIn(0.0f, 1.0f) } }
findViewById<Button>(R.id.validationVisualFadeDown).setOnClickListener { setVisualFloat("raster-particle-trail-fade", trailFade - 0.05f) { trailFade = it.coerceIn(0.0f, 0.995f) } }
findViewById<Button>(R.id.validationVisualFadeUp).setOnClickListener { setVisualFloat("raster-particle-trail-fade", trailFade + 0.05f) { trailFade = it.coerceIn(0.0f, 0.995f) } }
findViewById<Button>(R.id.validationVisualColor).setOnClickListener { setSpeedColor(!speedColorEnabled) }
findViewById<Button>(R.id.validationDateline).setOnClickListener {
maplibreMap.cameraPosition = CameraPosition.Builder().target(LatLng(0.0, 179.0)).zoom(2.0).build()
Log.i(TAG, "ParticleValidation30_03: DATELINE preset center=0,179 zoom=2")
@@ -196,6 +226,45 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
Log.i(TAG, "ParticleGPU: event=PARTICLE_COUNT_APPLIED requestedParticleCount=$requestedParticleCount effectiveParticleCount=$particleCount styleEpoch=$styleLoadEpoch")
}
private fun setVisualFloat(property: String, requested: Float, apply: (Float) -> Unit) {
if (!styleReady || !enabled) {
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=${if (!styleReady) "STYLE_LOADING" else "DISABLED"} property=$property styleEpoch=$styleLoadEpoch")
return
}
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REQUEST property=$property value=$requested styleEpoch=$styleLoadEpoch")
if (!maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, property, requested)) {
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=CORE_PROPERTY_REJECTED property=$property styleEpoch=$styleLoadEpoch")
return
}
apply(requested)
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_ONLY property=$property value=$requested styleEpoch=$styleLoadEpoch")
}
private fun setSpeedColor(requested: Boolean) {
if (!styleReady || !enabled) {
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=${if (!styleReady) "STYLE_LOADING" else "DISABLED"} property=raster-particle-speed-color-enabled styleEpoch=$styleLoadEpoch")
return
}
val value = if (requested) 1.0f else 0.0f
if (!maplibreMap.setStyleLayerFloatProperty(PARTICLE_LAYER_ID, "raster-particle-speed-color-enabled", value)) {
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_REJECTED reason=CORE_PROPERTY_REJECTED property=raster-particle-speed-color-enabled styleEpoch=$styleLoadEpoch")
return
}
speedColorEnabled = requested
status.text = statusText()
Log.i(TAG, "ParticleGPU: event=VISUAL_PROPERTY_ONLY property=raster-particle-speed-color-enabled value=$value styleEpoch=$styleLoadEpoch")
}
private fun resetVisualDefaults() {
setVisualFloat("raster-particle-line-width", DEFAULT_LINE_WIDTH) { lineWidth = DEFAULT_LINE_WIDTH }
setVisualFloat("raster-particle-particle-length", DEFAULT_PARTICLE_LENGTH) { particleLength = DEFAULT_PARTICLE_LENGTH }
setVisualFloat("raster-particle-particle-opacity", DEFAULT_PARTICLE_OPACITY) { particleOpacity = DEFAULT_PARTICLE_OPACITY }
setVisualFloat("raster-particle-trail-opacity", DEFAULT_TRAIL_OPACITY) { trailOpacity = DEFAULT_TRAIL_OPACITY }
setVisualFloat("raster-particle-trail-fade", DEFAULT_TRAIL_FADE) { trailFade = DEFAULT_TRAIL_FADE }
setSpeedColor(true)
}
private fun advanceRuntimeFieldWindow() {
val rejectReason = when {
!styleReady -> "STYLE_LOADING"
@@ -246,10 +315,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
"RUNTIME WINDOW"
} else if (fixtureMode == FIXTURE_UNIFORM_NORTH) {
"NORTH"
} else if (fixtureMode == FIXTURE_VORTEX) {
"VORTEX"
} else {
"EAST"
}
return "$mask · $runtime · $fields · MIX ${(timeMix * 100).toInt()}% · $particleCount particles"
return "$mask · $runtime · $fields · MIX ${(timeMix * 100).toInt()}% · $particleCount · W ${"%.1f".format(lineWidth)} L ${"%.3f".format(particleLength)} O ${(particleOpacity * 100).toInt()} T ${(trailOpacity * 100).toInt()} F ${(trailFade * 100).toInt()} C ${if (speedColorEnabled) "ON" else "OFF"}"
}
private fun validationStyle(active: Boolean, enabled: Boolean): String {
@@ -262,7 +333,10 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
val particleLayer = if (enabled) {
"""{"id":"particle-raster-core","type":"raster-particle","source":"particle-fixture",
"paint":{"raster-particle-active":$active,"raster-particle-mask-mode":$maskMode,
"raster-particle-time-mix":$timeMix,"raster-particle-particle-count":$particleCount}},"""
"raster-particle-time-mix":$timeMix,"raster-particle-particle-count":$particleCount,
"raster-particle-line-width":$lineWidth,"raster-particle-particle-length":$particleLength,
"raster-particle-particle-opacity":$particleOpacity,"raster-particle-trail-opacity":$trailOpacity,
"raster-particle-trail-fade":$trailFade,"raster-particle-speed-color-enabled":$speedColorEnabled}},"""
} else {
""
}
@@ -317,6 +391,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
outState.putInt(STATE_FIXTURE_MODE, fixtureMode)
outState.putFloat(STATE_TIME_MIX, timeMix)
outState.putInt(STATE_PARTICLE_COUNT, particleCount)
outState.putFloat(STATE_LINE_WIDTH, lineWidth)
outState.putFloat(STATE_PARTICLE_LENGTH, particleLength)
outState.putFloat(STATE_PARTICLE_OPACITY, particleOpacity)
outState.putFloat(STATE_TRAIL_OPACITY, trailOpacity)
outState.putFloat(STATE_TRAIL_FADE, trailFade)
outState.putBoolean(STATE_SPEED_COLOR, speedColorEnabled)
outState.putBoolean(STATE_TIME_WINDOW, timeWindowMode)
super.onSaveInstanceState(outState)
mapView.onSaveInstanceState(outState)
@@ -324,6 +404,7 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private fun fixtureId(): String = when (fixtureMode) {
FIXTURE_UNIFORM_NORTH -> "uniform-north"
FIXTURE_VORTEX -> "vortex"
else -> "uniform-east"
}
@@ -335,6 +416,12 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val STATE_FIXTURE_MODE = "particle_validation.fixture_mode"
private const val STATE_TIME_MIX = "particle_validation.time_mix"
private const val STATE_PARTICLE_COUNT = "particle_validation.particle_count"
private const val STATE_LINE_WIDTH = "particle_validation.line_width"
private const val STATE_PARTICLE_LENGTH = "particle_validation.particle_length"
private const val STATE_PARTICLE_OPACITY = "particle_validation.particle_opacity"
private const val STATE_TRAIL_OPACITY = "particle_validation.trail_opacity"
private const val STATE_TRAIL_FADE = "particle_validation.trail_fade"
private const val STATE_SPEED_COLOR = "particle_validation.speed_color"
private const val STATE_TIME_WINDOW = "particle_validation.time_window"
private const val PARTICLE_LAYER_ID = "particle-raster-core"
private const val PARTICLE_SOURCE_ID = "particle-fixture"
@@ -343,11 +430,18 @@ class ParticleCoreLayerValidationActivity : AppCompatActivity() {
private const val MASK_OCEAN_B = 2
private const val FIXTURE_UNIFORM_EAST = 0
private const val FIXTURE_UNIFORM_NORTH = 1
private const val FIXTURE_VORTEX = 2
private const val PARTICLE_COUNT_256 = 256
private const val PARTICLE_COUNT_1024 = 1024
private const val PARTICLE_COUNT_4096 = 4096
private const val PARTICLE_COUNT_16384 = 16384
private const val PARTICLE_COUNT_65536 = 65536
private const val PARTICLE_COUNT_262144 = 262144
private const val PARTICLE_COUNT_1048576 = 1048576
private const val DEFAULT_LINE_WIDTH = 2.0f
private const val DEFAULT_PARTICLE_LENGTH = 0.018f
private const val DEFAULT_PARTICLE_OPACITY = 1.0f
private const val DEFAULT_TRAIL_OPACITY = 1.0f
private const val DEFAULT_TRAIL_FADE = 0.92f
}
}

View File

@@ -47,9 +47,43 @@
<Button android:id="@+id/validationParticle16384" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_16384" />
<Button android:id="@+id/validationParticle65536" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_65536" />
<Button android:id="@+id/validationParticle262144" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_262144" />
<Button android:id="@+id/validationParticle1048576" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_particles_1048576" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="34dp"
android:orientation="horizontal">
<Button android:id="@+id/validationVisualDefault" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_default" />
<Button android:id="@+id/validationVisualSizeDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_size_down" />
<Button android:id="@+id/validationVisualSizeUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_size_up" />
<Button android:id="@+id/validationVisualLengthDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_length_down" />
<Button android:id="@+id/validationVisualLengthUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_length_up" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="34dp"
android:orientation="horizontal">
<Button android:id="@+id/validationVisualOpacityDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_opacity_down" />
<Button android:id="@+id/validationVisualOpacityUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_opacity_up" />
<Button android:id="@+id/validationVisualTrailDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_trail_down" />
<Button android:id="@+id/validationVisualTrailUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_trail_up" />
<Button android:id="@+id/validationVisualColor" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_color" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="34dp"
android:orientation="horizontal">
<Button android:id="@+id/validationVisualFadeDown" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_fade_down" />
<Button android:id="@+id/validationVisualFadeUp" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_visual_fade_up" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="34dp"
@@ -57,6 +91,7 @@
<Button android:id="@+id/validationUniformEast" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_uniform_east" />
<Button android:id="@+id/validationUniformNorth" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_uniform_north" />
<Button android:id="@+id/validationVortex" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_vortex" />
<Button android:id="@+id/validationTimeWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_time_window" />
<Button android:id="@+id/validationAdvanceWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_advance_window" />
<Button android:id="@+id/validationDuplicateWindow" style="@style/ParticleValidationButton" android:layout_width="0dp" android:layout_weight="1" android:text="@string/particle_validation_duplicate_window" />

View File

@@ -24,6 +24,7 @@
<string name="particle_validation_world_wrap">WORLD WRAP</string>
<string name="particle_validation_uniform_east">UNIFORM EAST</string>
<string name="particle_validation_uniform_north">UNIFORM NORTH</string>
<string name="particle_validation_vortex">VORTEX</string>
<string name="particle_validation_time_window">T0 EAST / T1 NORTH</string>
<string name="particle_validation_advance_window">ADVANCE WINDOW</string>
<string name="particle_validation_duplicate_window">DUPLICATE T1</string>
@@ -35,7 +36,20 @@
<string name="particle_validation_particles_4096">4K</string>
<string name="particle_validation_particles_16384">16K</string>
<string name="particle_validation_particles_65536">64K</string>
<string name="particle_validation_particles_262144">256K</string>
<string name="particle_validation_particles_1048576">1M</string>
<string name="particle_validation_visual_default">VIS DEFAULT</string>
<string name="particle_validation_visual_size_down">SIZE −</string>
<string name="particle_validation_visual_size_up">SIZE +</string>
<string name="particle_validation_visual_length_down">LENGTH −</string>
<string name="particle_validation_visual_length_up">LENGTH +</string>
<string name="particle_validation_visual_opacity_down">OPACITY −</string>
<string name="particle_validation_visual_opacity_up">OPACITY +</string>
<string name="particle_validation_visual_trail_down">TRAIL −</string>
<string name="particle_validation_visual_trail_up">TRAIL +</string>
<string name="particle_validation_visual_fade_down">FADE −</string>
<string name="particle_validation_visual_fade_up">FADE +</string>
<string name="particle_validation_visual_color">COLOR</string>
<string name="particle_validation_global_active_status">40-01 GLOBAL ACTIVE: offline field simulation and current-frame composite.</string>
<string name="particle_validation_global_static_status">40-01 GLOBAL STATIC: zero-step bootstrap and frozen composite.</string>
<string name="particle_validation_ocean_a_status">40-01 OCEAN_ONLY_A: shared coastal-mask consumer.</string>