Compare commits
3 Commits
1e7215e2a0
...
e8b451fe70
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b451fe70 | ||
|
|
95f0a45d6d | ||
|
|
baf9374318 |
@@ -245,6 +245,10 @@ public:
|
||||
particle::ParticleProjectionResult resolveParticleProjection() const;
|
||||
bool clearEncodedField(const std::string& fieldKey);
|
||||
void clearAllEncodedFields();
|
||||
// Explicit terminal cleanup for an attached WeatherRuntime. Unlike an ordinary
|
||||
// V4 window clear, this also drops bindings, forecast clock and V3 host ingress.
|
||||
// Source and Style layers remain installed for the next runtime generation.
|
||||
bool resetWeatherRuntimeState();
|
||||
// Producer-facing timestamp seek. Core alone selects T0/T1 and MIX.
|
||||
bool seekRuntimeTimelineToTimestamp(uint64_t timestamp);
|
||||
// Low-frequency JNI commands for the Source-owned validation timeline.
|
||||
|
||||
@@ -6,7 +6,7 @@ const marker = "// MapLibreParticleRNTestHost NavSea Particle release SDK";
|
||||
module.exports = function withParticleNativeAndroid(config) {
|
||||
return withProjectBuildGradle(config, (project) => {
|
||||
if (project.modResults.contents.includes(marker)) return project;
|
||||
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
project.modResults.contents += `\n${marker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
return project;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 13.4.1-particle.17(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 17。
|
||||
## 13.4.1-particle.16(Android / iOS 统一发行)
|
||||
|
||||
- Mac unified release pipeline 统一生成 Android Native、RN package 与 iOS SwiftPM/XCFramework;共享 Particle revision 16。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# @navsea/maplibre-particle-rn
|
||||
|
||||
当前版本:`13.4.1-particle.16`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
当前版本:`13.4.1-particle.17`。Android Native 必须配套 `cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17`。本包是 React Native 的薄控制面,不负责 Map 组件、provider、HTTP、manifest、下载、缓存或 NavSea 业务策略。
|
||||
|
||||
## 当前推荐主链:QBIN V4
|
||||
|
||||
@@ -37,7 +37,7 @@ Podfile 的 `post_install do |installer|` 必须同时调用 `$MLRN.post_install
|
||||
RN tgz:
|
||||
|
||||
```text
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.16.tgz
|
||||
https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz
|
||||
```
|
||||
|
||||
Native Maven repository:
|
||||
@@ -49,7 +49,7 @@ https://pbf.jpcn.ccwu.cc/maven
|
||||
Native coordinate:
|
||||
|
||||
```text
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16
|
||||
cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17
|
||||
```
|
||||
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.16`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
Expo plugin 与 `android/build.gradle` 必须指向同一 `.17`。正常 Consumer 不复制 AAR、不依赖 MapLibreParticleLab 源码路径,也不以 Maven Local 作为公网验证主路径。完整 Linux 发布与 Win11 验证流程见 `docs/50_Step记录/STEP-080_Android_AAR与RNTestHost标准构建流水线_v2.md`。
|
||||
|
||||
@@ -14,7 +14,7 @@ android {
|
||||
dependencies {
|
||||
implementation "com.facebook.react:react-android"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:2.1.20"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16"
|
||||
implementation "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17"
|
||||
compileOnly project(":maplibre_maplibre-react-native")
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
),
|
||||
)
|
||||
}
|
||||
val runtime = map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
|
||||
val runtime = try {
|
||||
map.replaceWeatherRuntime(WeatherRuntime.Config(WeatherIds.SOURCE, WeatherIds.COLOR_LAYER, WeatherIds.PARTICLE_LAYER))
|
||||
} catch (error: IllegalStateException) {
|
||||
return@withMapView reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime replacement cleanup failed")
|
||||
}
|
||||
if (!attachmentOwner(view).attach(map)) {
|
||||
map.disposeWeatherRuntime()
|
||||
return@withMapView reject(promise, "RUNTIME_DISPOSED", "The MapView attachment owner has been destroyed")
|
||||
@@ -57,11 +61,18 @@ class WeatherRuntimeBridgeModule(private val context: ReactApplicationContext) :
|
||||
@ReactMethod
|
||||
override fun detach(reactTag: Double, promise: Promise) = withMapView(reactTag, promise) { view, map ->
|
||||
val owner = view.getTag(R.id.weather_runtime_attachment_owner) as? WeatherRuntimeAttachmentOwner<*>
|
||||
val hasRuntime = map.weatherRuntime != null
|
||||
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
|
||||
if (!detachedByOwner) map.disposeWeatherRuntime()
|
||||
if (!hasRuntime) reject(promise, "RUNTIME_DISPOSED", "No attached WeatherRuntime exists for this map")
|
||||
else promise.resolve(null)
|
||||
// Duplicate detach is idempotent; Core failures must not be reported as success.
|
||||
try {
|
||||
val detachedByOwner = (owner as? WeatherRuntimeAttachmentOwner<MapLibreMap>)?.detach(map) ?: false
|
||||
if (!detachedByOwner) map.disposeWeatherRuntime()
|
||||
if (map.weatherRuntime != null) {
|
||||
reject(promise, "RUNTIME_CLEANUP_FAILED", "WeatherRuntime remains attached after detach")
|
||||
} else {
|
||||
promise.resolve(null)
|
||||
}
|
||||
} catch (error: IllegalStateException) {
|
||||
reject(promise, "RUNTIME_CLEANUP_FAILED", error.message ?: "WeatherRuntime detach cleanup failed")
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod override fun getState(reactTag: Double, promise: Promise) = withRuntime(reactTag, promise) { runtime -> promise.resolve(snapshot(runtime)) }
|
||||
|
||||
@@ -20,7 +20,7 @@ const iosTag = "navsea-maplibre-particle-rn-maplibre-spm";
|
||||
module.exports = function withNavSeaMapLibreParticle(config) {
|
||||
config = withProjectBuildGradle(config, (project) => {
|
||||
if (project.modResults.contents.includes(androidMarker)) return project;
|
||||
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
project.modResults.contents += `\n${androidMarker}\ngradle.projectsEvaluated {\n allprojects { p ->\n p.repositories.maven { repository -> repository.url = p.uri("https://pbf.jpcn.ccwu.cc/maven") }\n p.configurations.configureEach { configuration ->\n configuration.resolutionStrategy.dependencySubstitution {\n substitute module("org.maplibre.gl:android-sdk-opengl") using module("cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17")\n }\n }\n }\n}\nrootProject.ext.set("org.maplibre.reactnative.nativeVersion", "13.4.1")\nrootProject.ext.set("org.maplibre.reactnative.nativeVariant", "opengl")\n`;
|
||||
return project;
|
||||
});
|
||||
|
||||
|
||||
@@ -155,7 +155,16 @@ RCT_EXPORT_MODULE(WeatherRuntimeBridge)
|
||||
}];
|
||||
}
|
||||
- (void)detach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { [map disposeParticleFieldRuntime]; resolve(nil); }];
|
||||
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
|
||||
// Duplicate detach is a no-op, but an actual Source cleanup failure is not.
|
||||
if (!runtime) { resolve(nil); return; }
|
||||
NSError *error = nil;
|
||||
if (![map disposeParticleFieldRuntimeWithError:&error]) {
|
||||
reject(@"RUNTIME_CLEANUP_FAILED", error.localizedDescription ?: @"WeatherRuntime detach cleanup failed", error);
|
||||
return;
|
||||
}
|
||||
resolve(nil);
|
||||
}];
|
||||
}
|
||||
- (void)getState:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { [self withMapForTag:reactTag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { resolve(snapshot(runtime)); }]; }
|
||||
- (void)setRetainedUnderlayEnabled:(double)reactTag enabled:(BOOL)enabled resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.16",
|
||||
"version": "13.4.1-particle.17",
|
||||
"description": "NavSea MapLibre Particle React Native pure Field Renderer API.",
|
||||
"license": "BSD-2-Clause",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -1472,6 +1472,16 @@ jni::jboolean NativeMapView::clearRasterArraySourceHostFields(JNIEnv& env, const
|
||||
return cleared;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::resetRasterArraySourceWeatherRuntime(JNIEnv& env, const jni::String& sourceId) {
|
||||
const auto id = jni::Make<std::string>(env, sourceId);
|
||||
auto* source = dynamic_cast<mbgl::style::RasterArraySource*>(map->getStyle().getSource(id));
|
||||
if (!source) return false;
|
||||
const bool reset = source->resetWeatherRuntimeState();
|
||||
mbgl::Log::Info(mbgl::Event::JNI, "WeatherRuntime: event=TERMINAL_RESET sourceId=" + id +
|
||||
" result=" + (reset ? "SUCCESS" : "REJECTED"));
|
||||
return reset;
|
||||
}
|
||||
|
||||
jni::jboolean NativeMapView::seekRasterArraySourceTimelineToTimestamp(JNIEnv& env,
|
||||
const jni::String& sourceId,
|
||||
const jni::jlong timestamp) {
|
||||
@@ -1920,6 +1930,7 @@ void NativeMapView::registerNative(jni::JNIEnv& env) {
|
||||
METHOD(&NativeMapView::submitRasterArraySourceHostField, "nativeSubmitRasterArraySourceHostField"),
|
||||
METHOD(&NativeMapView::submitRasterArraySourceHostFieldV2, "nativeSubmitRasterArraySourceHostFieldV2"),
|
||||
METHOD(&NativeMapView::clearRasterArraySourceHostFields, "nativeClearRasterArraySourceHostFields"),
|
||||
METHOD(&NativeMapView::resetRasterArraySourceWeatherRuntime, "nativeResetRasterArraySourceWeatherRuntime"),
|
||||
METHOD(&NativeMapView::seekRasterArraySourceTimelineToTimestamp, "nativeSeekRasterArraySourceTimelineToTimestamp"),
|
||||
METHOD(&NativeMapView::controlRasterArraySourceTimeline, "nativeControlRasterArraySourceTimeline"),
|
||||
METHOD(&NativeMapView::submitRasterArraySourceEncodedField, "nativeSubmitRasterArraySourceEncodedField"),
|
||||
|
||||
@@ -318,6 +318,7 @@ public:
|
||||
jni::jint,
|
||||
jni::jint);
|
||||
jni::jboolean clearRasterArraySourceHostFields(JNIEnv&, const jni::String&);
|
||||
jni::jboolean resetRasterArraySourceWeatherRuntime(JNIEnv&, const jni::String&);
|
||||
jni::jboolean seekRasterArraySourceTimelineToTimestamp(JNIEnv&, const jni::String&, jni::jlong);
|
||||
jni::jboolean controlRasterArraySourceTimeline(JNIEnv&, const jni::String&, jni::jint, jni::jfloat);
|
||||
jni::jint submitRasterArraySourceEncodedField(JNIEnv&, const jni::String&, const jni::String&, const jni::String&, jni::jlong, jni::jlong, const jni::String&, const jni::Array<jbyte>&);
|
||||
|
||||
@@ -491,6 +491,11 @@ public final class MapLibreMap {
|
||||
return nativeMapView.clearRasterArraySourceHostFields(sourceId);
|
||||
}
|
||||
|
||||
/** Internal terminal reset for V3+V4 state on the same existing Style source. */
|
||||
boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
|
||||
return nativeMapView.resetRasterArraySourceWeatherRuntime(sourceId);
|
||||
}
|
||||
|
||||
/** Requests a Core-owned T0/T1/MIX selection for a producer timestamp in milliseconds. */
|
||||
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
|
||||
return nativeMapView.seekRasterArraySourceTimelineToTimestamp(sourceId, timestamp);
|
||||
|
||||
@@ -172,6 +172,7 @@ interface NativeMap {
|
||||
boolean vectorPresent, int vectorLength, int validityLength);
|
||||
|
||||
boolean clearRasterArraySourceHostFields(String sourceId);
|
||||
boolean resetRasterArraySourceWeatherRuntime(String sourceId);
|
||||
|
||||
boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
|
||||
|
||||
|
||||
@@ -1070,6 +1070,12 @@ final class NativeMapView implements NativeMap {
|
||||
&& nativeClearRasterArraySourceHostFields(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resetRasterArraySourceWeatherRuntime(String sourceId) {
|
||||
return !checkState("resetRasterArraySourceWeatherRuntime")
|
||||
&& nativeResetRasterArraySourceWeatherRuntime(sourceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean seekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp) {
|
||||
return !checkState("seekRasterArraySourceTimelineToTimestamp")
|
||||
@@ -1876,6 +1882,9 @@ final class NativeMapView implements NativeMap {
|
||||
@Keep
|
||||
private native boolean nativeClearRasterArraySourceHostFields(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeResetRasterArraySourceWeatherRuntime(String sourceId);
|
||||
|
||||
@Keep
|
||||
private native boolean nativeSeekRasterArraySourceTimelineToTimestamp(String sourceId, long timestamp);
|
||||
|
||||
|
||||
@@ -81,7 +81,25 @@ public final class WeatherRuntime {
|
||||
public Config getConfig() { return config; }
|
||||
public boolean isDisposed() { return disposed; }
|
||||
/** Ends this map/style-scoped facade. It is safe to call repeatedly. */
|
||||
public void dispose() { if (disposed) return; disposed = true; cancelCadenceWake(); }
|
||||
public void dispose() {
|
||||
if (disposed) return;
|
||||
cancelCadenceWake();
|
||||
// If Style has already dropped the source, there is no Source-owned V4 store
|
||||
// left to reset; otherwise disposal must not claim success before Core reset.
|
||||
if (map.hasStyleSource(config.sourceId)) {
|
||||
map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
||||
map.setStyleLayerVisibility(config.particleLayerId, false);
|
||||
if (!map.resetRasterArraySourceWeatherRuntime(config.sourceId)) {
|
||||
throw new IllegalStateException("WeatherRuntime terminal Core reset failed");
|
||||
}
|
||||
}
|
||||
submittedEncodedFieldKeys.clear();
|
||||
v4ParticleBindingFieldKey = null;
|
||||
v4ForecastPlaying = false;
|
||||
playbackState = PlaybackState.PAUSE;
|
||||
state = State.DISABLED;
|
||||
disposed = true;
|
||||
}
|
||||
/** Core-owned capacity; Host never sees the shader's UBO layout. */
|
||||
public int getWeatherColorStopCapacity() { return weatherColorStopCapacity; }
|
||||
|
||||
@@ -154,8 +172,14 @@ public final class WeatherRuntime {
|
||||
map.controlRasterArraySourceTimeline(config.sourceId, 1, 0f);
|
||||
boolean color = map.setStyleLayerVisibility(config.weatherColorLayerId, false);
|
||||
boolean particle = map.setStyleLayerVisibility(config.particleLayerId, false);
|
||||
boolean clear = map.clearRasterArraySourceHostFields(config.sourceId);
|
||||
boolean clear = map.resetRasterArraySourceWeatherRuntime(config.sourceId);
|
||||
state = stateAfterDisable(color && particle && clear);
|
||||
if (clear) {
|
||||
submittedEncodedFieldKeys.clear();
|
||||
v4ParticleBindingFieldKey = null;
|
||||
v4ForecastPlaying = false;
|
||||
playbackState = PlaybackState.PAUSE;
|
||||
}
|
||||
if (state != State.READY) cancelCadenceWake();
|
||||
return result(color && particle && clear);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,40 @@ public class WeatherRuntimeStateTest {
|
||||
Mockito.verify(map, Mockito.never()).submitRasterArraySourceHostField(Mockito.anyString(), Mockito.anyString(), Mockito.any(byte[].class));
|
||||
}
|
||||
|
||||
@Test public void disableResetsV3AndV4ViaOneCoreCommandAndCanEnableAgain() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
Mockito.when(map.setStyleLayerVisibility(Mockito.anyString(), Mockito.anyBoolean())).thenReturn(true);
|
||||
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(true);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(false));
|
||||
assertEquals(WeatherRuntime.State.DISABLED, runtime.getState());
|
||||
Mockito.verify(map).resetRasterArraySourceWeatherRuntime("source");
|
||||
Mockito.verify(map, Mockito.never()).clearRasterArraySourceHostFields(Mockito.anyString());
|
||||
assertEquals(WeatherRuntime.CommandResult.ACCEPTED, runtime.setWeatherEnabled(true));
|
||||
assertEquals(WeatherRuntime.State.WAITING_FOR_FIELDS, runtime.getState());
|
||||
}
|
||||
|
||||
@Test public void terminalDisposeResetsSourceAndFailureDoesNotClaimDisposed() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
Mockito.when(map.hasStyleSource("source")).thenReturn(true);
|
||||
Mockito.when(map.resetRasterArraySourceWeatherRuntime("source")).thenReturn(false).thenReturn(true);
|
||||
WeatherRuntime runtime = new WeatherRuntime(map, new WeatherRuntime.Config("source", "color", "particle"));
|
||||
try {
|
||||
runtime.dispose();
|
||||
org.junit.Assert.fail("Core reset failure must be reported");
|
||||
} catch (IllegalStateException expected) {
|
||||
assertEquals(false, runtime.isDisposed());
|
||||
}
|
||||
runtime.dispose();
|
||||
assertEquals(true, runtime.isDisposed());
|
||||
Mockito.verify(map, Mockito.times(2)).resetRasterArraySourceWeatherRuntime("source");
|
||||
Mockito.clearInvocations(map);
|
||||
runtime.dispose();
|
||||
Mockito.verifyNoInteractions(map);
|
||||
}
|
||||
|
||||
@Test public void disposeIsIdempotentAndRejectsFurtherControls() {
|
||||
MapLibreMap map = Mockito.mock(MapLibreMap.class);
|
||||
Mockito.when(map.getWeatherColorStopCapacity()).thenReturn(8);
|
||||
|
||||
@@ -1 +1 @@
|
||||
13.4.1-particle.16
|
||||
13.4.1-particle.17
|
||||
|
||||
@@ -131,7 +131,8 @@ MLN_EXPORT
|
||||
@property (nonatomic, readonly) MLNParticleFieldRuntimeTargetFps targetFps;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (void)dispose;
|
||||
- (void)dispose; // Existing API; use the error-returning form for Bridge operations.
|
||||
- (BOOL)disposeWithError:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
- (MLNParticleHostFieldSubmitResult)submitWindFieldWithMetadataJSON:(NSString *)metadataJSON payload:(NSData *)payload error:(NSError * _Nullable * _Nullable)error;
|
||||
- (MLNParticleHostFieldSubmitResult)submitFieldWithMetadataJSON:(NSString *)metadataJSON scalar:(nullable NSData *)scalar vector:(nullable NSData *)vector validity:(NSData *)validity error:(NSError * _Nullable * _Nullable)error;
|
||||
@@ -196,7 +197,11 @@ MLN_EXPORT
|
||||
- (nullable MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration
|
||||
error:(NSError * _Nullable * _Nullable)error;
|
||||
- (nullable MLNParticleFieldRuntime *)particleFieldRuntime;
|
||||
// Legacy public selector retained for existing Native consumers.
|
||||
- (void)disposeParticleFieldRuntime;
|
||||
// Completes V3+V4 Source cleanup before detaching; a failure leaves the runtime
|
||||
// associated so the caller may retry and must not be reported as success.
|
||||
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError * _Nullable * _Nullable)error;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -138,11 +138,36 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
- (MLNParticleFieldRuntimePlaybackState)playbackState { return _playbackState; }
|
||||
- (MLNParticleFieldRuntimeTargetFps)targetFps { return _targetFps; }
|
||||
|
||||
- (void)dispose {
|
||||
- (BOOL)disposeWithError:(NSError **)error {
|
||||
if (_disposed) return YES;
|
||||
// A terminal dispose must clear the Source while the map and Style are valid.
|
||||
// Otherwise the Source outlives this ObjC runtime and retains V4 field windows.
|
||||
[_v4CadenceTimer invalidate];
|
||||
_v4CadenceTimer = nil;
|
||||
_v4ForecastPlaying = NO;
|
||||
const BOOL completed = [self withCurrentSource:error action:^bool(
|
||||
mbgl::style::RasterArraySource &source,
|
||||
mbgl::style::WeatherColorLayer &weather,
|
||||
mbgl::style::RasterParticleLayer &particle) {
|
||||
weather.setVisibility(mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(mbgl::style::VisibilityType::None);
|
||||
return source.resetWeatherRuntimeState();
|
||||
}];
|
||||
if (!completed) return NO;
|
||||
[_submittedEncodedFieldKeys removeAllObjects];
|
||||
_v4ParticleBindingFieldKey = nil;
|
||||
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
|
||||
_state = MLNParticleFieldRuntimeStateDisabled;
|
||||
_disposed = YES;
|
||||
_mapView = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)dispose {
|
||||
NSError *error = nil;
|
||||
if (![self disposeWithError:&error]) {
|
||||
NSLog(@"WeatherRuntime: legacy dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)refreshV4CadenceWake {
|
||||
@@ -355,11 +380,34 @@ std::vector<char> charsFromData(NSData *data) {
|
||||
if (_disposed) return MLNParticleFieldRuntimeCommandResultRejected;
|
||||
if (enabled && _state == MLNParticleFieldRuntimeStateError) return MLNParticleFieldRuntimeCommandResultRejected;
|
||||
if (enabled && _state != MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
|
||||
__block bool accepted = false; BOOL completed = [self withCurrentSource:error action:^bool(mbgl::style::RasterArraySource &source, mbgl::style::WeatherColorLayer &weather, mbgl::style::RasterParticleLayer &particle) {
|
||||
if (!enabled) { source.controlRuntimeTimeline(1); weather.setVisibility(mbgl::style::VisibilityType::None); particle.setVisibility(mbgl::style::VisibilityType::None); accepted = source.clearHostFields(); }
|
||||
else { weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None); accepted = true; } return true;
|
||||
if (!enabled && _state == MLNParticleFieldRuntimeStateDisabled) return MLNParticleFieldRuntimeCommandResultAccepted;
|
||||
__block bool accepted = false;
|
||||
BOOL completed = [self withCurrentSource:error action:^bool(
|
||||
mbgl::style::RasterArraySource &source,
|
||||
mbgl::style::WeatherColorLayer &weather,
|
||||
mbgl::style::RasterParticleLayer &particle) {
|
||||
if (!enabled) {
|
||||
weather.setVisibility(mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(mbgl::style::VisibilityType::None);
|
||||
accepted = source.resetWeatherRuntimeState();
|
||||
} else {
|
||||
weather.setVisibility(_weatherColorVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
|
||||
particle.setVisibility(_particleVisible ? mbgl::style::VisibilityType::Visible : mbgl::style::VisibilityType::None);
|
||||
accepted = true;
|
||||
}
|
||||
return accepted;
|
||||
}];
|
||||
if (completed && accepted) _state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled; else if (completed) _state = MLNParticleFieldRuntimeStateError;
|
||||
if (completed && accepted) {
|
||||
_state = enabled ? MLNParticleFieldRuntimeStateWaitingForFields : MLNParticleFieldRuntimeStateDisabled;
|
||||
if (!enabled) {
|
||||
[_submittedEncodedFieldKeys removeAllObjects];
|
||||
_v4ParticleBindingFieldKey = nil;
|
||||
_v4ForecastPlaying = NO;
|
||||
_playbackState = MLNParticleFieldRuntimePlaybackStatePause;
|
||||
}
|
||||
} else if (completed) {
|
||||
_state = MLNParticleFieldRuntimeStateError;
|
||||
}
|
||||
[self refreshV4CadenceWake];
|
||||
return commandResult(completed && accepted);
|
||||
}
|
||||
@@ -400,7 +448,7 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
|
||||
@implementation MLNMapView (ParticleFieldRuntime)
|
||||
|
||||
- (MLNParticleFieldRuntime *)replaceParticleFieldRuntimeWithConfiguration:(MLNParticleFieldRuntimeConfiguration *)configuration error:(NSError **)error {
|
||||
[self disposeParticleFieldRuntime];
|
||||
if (![self disposeParticleFieldRuntimeWithError:error]) return nil;
|
||||
if (!configuration) { setError(error, MLNParticleFieldRuntimeErrorInvalidArgument, @"A particle field runtime configuration is required."); return nil; }
|
||||
MLNParticleFieldRuntime *runtime = [[MLNParticleFieldRuntime alloc] initWithMapView:self configuration:configuration error:error];
|
||||
if (!runtime) return nil;
|
||||
@@ -413,9 +461,18 @@ namespace { const void *ParticleFieldRuntimeAssociationKey = &ParticleFieldRunti
|
||||
}
|
||||
|
||||
- (void)disposeParticleFieldRuntime {
|
||||
NSError *error = nil;
|
||||
if (![self disposeParticleFieldRuntimeWithError:&error]) {
|
||||
NSLog(@"WeatherRuntime: legacy map dispose cleanup failed: %@", error.localizedDescription ?: @"unknown");
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)disposeParticleFieldRuntimeWithError:(NSError **)error {
|
||||
MLNParticleFieldRuntime *runtime = self.particleFieldRuntime;
|
||||
[runtime dispose];
|
||||
if (!runtime) return YES;
|
||||
if (![runtime disposeWithError:error]) return NO;
|
||||
objc_setAssociatedObject(self, ParticleFieldRuntimeAssociationKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,8 +9,8 @@ let package = Package(
|
||||
targets: [
|
||||
.binaryTarget(
|
||||
name: "MapLibre",
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8"
|
||||
url: "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip",
|
||||
checksum: "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be"
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# MapLibre Particle iOS 发行
|
||||
|
||||
当前正式发行:6.28.0-particle.16
|
||||
当前正式发行:6.28.0-particle.17
|
||||
|
||||
- Renderer:Metal
|
||||
- Product / Module:MapLibre
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8
|
||||
- Binary:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip
|
||||
- SHA-256:5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be
|
||||
- SwiftPM:https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git
|
||||
- Exact Version:6.28.0-particle.16
|
||||
- Source Commit:e9f84a8ec6aa99a308af30c1957e83c31ec5f23f
|
||||
- Exact Version:6.28.0-particle.17
|
||||
- Source Commit:95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3
|
||||
|
||||
构建命令固定为:
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"status": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"distributionVersion": "6.28.0-particle.16",
|
||||
"distributionVersion": "6.28.0-particle.17",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8442202,
|
||||
"sha256": "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8443308,
|
||||
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"sourceCommit": "e9f84a8ec6aa99a308af30c1957e83c31ec5f23f",
|
||||
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.16",
|
||||
"version": "6.28.0-particle.17",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"notes": "Metal renderer release built with bazel build --//:renderer=metal //platform/ios:MapLibre.dynamic"
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"releaseState": "PUBLISHED_PUBLIC_READBACK_PASS",
|
||||
"particleRevision": 16,
|
||||
"sourceCommit": "e9f84a8ec6aa99a308af30c1957e83c31ec5f23f",
|
||||
"generatedAtUtc": "2026-09-27T13:34:02.735946+00:00",
|
||||
"particleRevision": 17,
|
||||
"sourceCommit": "95f0a45d6d97c55f0fd417cf4d7a9a3746ab5aa3",
|
||||
"generatedAtUtc": "2026-09-29T00:43:56.789316+00:00",
|
||||
"reactNative": {
|
||||
"name": "@navsea/maplibre-particle-rn",
|
||||
"version": "13.4.1-particle.16",
|
||||
"version": "13.4.1-particle.17",
|
||||
"artifact": {
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.16.tgz",
|
||||
"sizeBytes": 28295,
|
||||
"sha256": "0016dd4719cbe02decaa940abbac2b2589448f0cc11f3944132d8e08627d627d",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.16.tgz"
|
||||
"fileName": "navsea-maplibre-particle-rn-13.4.1-particle.17.tgz",
|
||||
"sizeBytes": 28526,
|
||||
"sha256": "7415a094da5351b86a3c6343a8bc2ef75b7a8ad49577a5f0efc81407d72a79ec",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/npm/navsea-maplibre-particle-rn-13.4.1-particle.17.tgz"
|
||||
},
|
||||
"mapLibreReactNative": "11.3.6"
|
||||
},
|
||||
"android": {
|
||||
"version": "13.4.1-particle.16",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.16",
|
||||
"version": "13.4.1-particle.17",
|
||||
"coordinate": "cc.navsea.maplibre:android-sdk-particle-opengl:13.4.1-particle.17",
|
||||
"renderer": "opengl",
|
||||
"artifact": {
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.16.aar",
|
||||
"sizeBytes": 170123246,
|
||||
"sha256": "d39f8f4d0f96898874a0869823e02a51ecd985a877d88407f10f8b0130892380",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.16/android-sdk-particle-opengl-13.4.1-particle.16.aar"
|
||||
"fileName": "android-sdk-particle-opengl-13.4.1-particle.17.aar",
|
||||
"sizeBytes": 170158515,
|
||||
"sha256": "6c5f10f3ddd8b81e1c14ed9c310f8d5e216dfdded32c3db55bfb0ba5f5d175d5",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/maven/cc/navsea/maplibre/android-sdk-particle-opengl/13.4.1-particle.17/android-sdk-particle-opengl-13.4.1-particle.17.aar"
|
||||
},
|
||||
"ndk": "28.2.13676358",
|
||||
"gradle": "9.5.1",
|
||||
"jdk": "openjdk version \"17.0.20.1\" 2026-08-18"
|
||||
},
|
||||
"ios": {
|
||||
"distributionVersion": "6.28.0-particle.16",
|
||||
"distributionVersion": "6.28.0-particle.17",
|
||||
"renderer": "metal",
|
||||
"moduleName": "MapLibre",
|
||||
"artifact": {
|
||||
"fileName": "MapLibre.dynamic.xcframework.zip",
|
||||
"sizeBytes": 8442202,
|
||||
"sha256": "88f5b8190f2c7479b54c4a4cb51de372b61902d2a074b9522ccaaefd4d27afb8",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.16/MapLibre.dynamic.xcframework.zip"
|
||||
"sizeBytes": 8443308,
|
||||
"sha256": "5535345dd480f80c0f083811abe93f011b41b4d1a03588189eec5c50a36af0be",
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle/6.28.0-particle.17/MapLibre.dynamic.xcframework.zip"
|
||||
},
|
||||
"swiftPackage": {
|
||||
"url": "https://pbf.jpcn.ccwu.cc/swift/maplibre-particle-spm.git",
|
||||
"requirement": "exactVersion",
|
||||
"version": "6.28.0-particle.16",
|
||||
"version": "6.28.0-particle.17",
|
||||
"productName": "MapLibre"
|
||||
},
|
||||
"xcode": "Xcode 26.3 Build version 17C529 ",
|
||||
|
||||
@@ -535,6 +535,37 @@ void RasterArraySource::clearAllEncodedFields() {
|
||||
refreshV4RendererState();
|
||||
}
|
||||
|
||||
bool RasterArraySource::resetWeatherRuntimeState() {
|
||||
// V3 Host ingress and V4 QBIN can coexist on the same raster-array Source.
|
||||
// Clearing the V3 window alone would leave the independent V4 store/projection.
|
||||
// Pause first so Source notifications during cleanup cannot advance the clock.
|
||||
forecastClock.pause();
|
||||
lastForecastTick = {};
|
||||
if (impl().hostFieldSubmission && !clearHostFields()) return false;
|
||||
|
||||
const auto oldFieldCount = encodedFieldStore.fieldCount();
|
||||
const auto oldDecodedBytes = encodedFieldStore.decodedByteSize();
|
||||
const auto hadColorProjection = static_cast<bool>(impl().v4RendererState.color);
|
||||
const auto hadParticleProjection = static_cast<bool>(impl().v4RendererState.particle);
|
||||
encodedFieldStore.clearAll();
|
||||
colorBinding.reset();
|
||||
particleBinding.reset();
|
||||
forecastClock = particle::ForecastClock{};
|
||||
lastForecastTick = {};
|
||||
refreshV4RendererState();
|
||||
Log::Info(Event::Render,
|
||||
"WeatherRuntime: event=V4_RUNTIME_RESET sourceId=" + getID() +
|
||||
" oldFieldCount=" + std::to_string(oldFieldCount) +
|
||||
" oldDecodedBytes=" + std::to_string(oldDecodedBytes) +
|
||||
" oldColorProjection=" + std::to_string(hadColorProjection) +
|
||||
" oldParticleProjection=" + std::to_string(hadParticleProjection) +
|
||||
" remainingFieldCount=" + std::to_string(encodedFieldStore.fieldCount()) +
|
||||
" remainingDecodedBytes=" + std::to_string(encodedFieldStore.decodedByteSize()) +
|
||||
" remainingColorProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.color)) +
|
||||
" remainingParticleProjection=" + std::to_string(static_cast<bool>(impl().v4RendererState.particle)));
|
||||
return true;
|
||||
}
|
||||
|
||||
void RasterArraySource::refreshV4RendererState() {
|
||||
Impl::V4RendererState next;
|
||||
const auto& current = impl().v4RendererState;
|
||||
|
||||
@@ -912,3 +912,60 @@ TEST(Source, RasterArrayV4MaterializerDecodesDirectionToUInt16LittleEndian) {
|
||||
EXPECT_NEAR(materialized->t0->vector[7], 0.0f, 1e-5f);
|
||||
EXPECT_EQ(materialized->t0->vectorSemantics, particle::VectorSemantics::Direction);
|
||||
}
|
||||
|
||||
|
||||
// The RN weather source uses Host V3 ingress and V4 QBIN on one Style source.
|
||||
// A terminal detach must clear both, drop old bindings, and permit a new owner.
|
||||
TEST(Source, RasterArrayV4TerminalResetClearsBothIngressesAndAllowsNewGeneration) {
|
||||
style::RasterArraySource source{"v4-detach-reset"};
|
||||
auto v3T0 = particle::OfflineFixtureParticleAdapter::load("uniform-east", 71);
|
||||
auto v3T1 = particle::OfflineFixtureParticleAdapter::load("uniform-north", 72);
|
||||
v3T1.validTime = v3T0.validTime + Hour;
|
||||
ASSERT_EQ(source.submitInitialRuntimeFieldWindow(v3T0, v3T1),
|
||||
style::RasterArrayRuntimeFieldUpdateResult::Applied);
|
||||
ASSERT_TRUE(source.impl().field);
|
||||
ASSERT_TRUE(source.impl().nextField);
|
||||
|
||||
submit(source, "wind", "wind", Base, 1);
|
||||
submit(source, "wind", "wind", Base + Hour, 2);
|
||||
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
particle::FieldValueClass::VelocityComponent, 1});
|
||||
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
||||
"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
||||
ASSERT_TRUE(source.impl().v4RendererState.color);
|
||||
ASSERT_TRUE(source.impl().v4RendererState.particle);
|
||||
ASSERT_EQ(source.impl().v4RendererState.color->sampleState.mix, 0.5f);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle->sampleState.mix, 0.5f);
|
||||
source.setForecastPlaybackState(particle::ForecastPlaybackState::Playing);
|
||||
ASSERT_EQ(source.impl().hostFieldSubmission, true);
|
||||
|
||||
ASSERT_TRUE(source.resetWeatherRuntimeState());
|
||||
EXPECT_FALSE(source.impl().field);
|
||||
EXPECT_FALSE(source.impl().nextField);
|
||||
EXPECT_FALSE(source.impl().timeline);
|
||||
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.colorInput);
|
||||
EXPECT_FALSE(source.impl().v4RendererState.particleInput);
|
||||
EXPECT_EQ(source.resolveColorProjection().error, particle::BindingProjectionError::InvalidBinding);
|
||||
EXPECT_EQ(source.resolveParticleProjection().error, particle::BindingProjectionError::InvalidBinding);
|
||||
EXPECT_EQ(source.impl().v4RendererState.color, nullptr);
|
||||
EXPECT_EQ(source.impl().v4RendererState.particle, nullptr);
|
||||
EXPECT_FALSE(source.tickForecastClock(std::chrono::steady_clock::now()));
|
||||
EXPECT_TRUE(source.resetWeatherRuntimeState()); // Idempotent.
|
||||
|
||||
submit(source, "wind", "wind", Base, 3);
|
||||
submit(source, "wind", "wind", Base + Hour, 4);
|
||||
source.setColorBinding({"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
particle::FieldValueClass::VelocityComponent, 1});
|
||||
source.setParticleBinding({particle::ParticleBindingMode::VectorComponents,
|
||||
"wind", {1, 1, particle::FieldComponent::UEast},
|
||||
{2, 1, particle::FieldComponent::VNorth}, {}, 1});
|
||||
ASSERT_TRUE(source.setRequestedForecastTime(Base + Hour / 2));
|
||||
EXPECT_TRUE(source.resolveColorProjection().accepted());
|
||||
EXPECT_TRUE(source.resolveParticleProjection().accepted());
|
||||
EXPECT_TRUE(source.impl().v4RendererState.color);
|
||||
EXPECT_TRUE(source.impl().v4RendererState.particle);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user