Files
MapLibreParticleLab/packages/maplibre-particle-rn/ios/WeatherRuntimeBridge.mm

270 lines
32 KiB
Plaintext

#import "WeatherRuntimeBridge.h"
#import <MapLibre/MapLibre.h>
#import <React/RCTBridgeModule.h>
using namespace facebook::react;
namespace {
constexpr NSUInteger MaxPayloadBytes = 64u * 1024u * 1024u;
constexpr NSUInteger MaxMetadataBytes = 1'048'576u;
constexpr NSUInteger MaxContractBytes = 256u * 1024u;
constexpr double MaxSafeInteger = 9007199254740991.0;
BOOL validIdentifier(NSString *value) {
const NSUInteger length = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
return length >= 1 && length <= 64;
}
BOOL validSafeInteger(double value, BOOL positive) {
return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= MaxSafeInteger;
}
BOOL validUInt16(double value, BOOL positive) {
return isfinite(value) && floor(value) == value && value >= (positive ? 1 : 0) && value <= UINT16_MAX;
}
NSString *commandString(MLNParticleFieldRuntimeCommandResult result) {
return result == MLNParticleFieldRuntimeCommandResultAccepted ? @"ACCEPTED" : @"REJECTED";
}
NSString *v3String(MLNParticleHostFieldSubmitResult result) {
switch (result) {
case MLNParticleHostFieldSubmitResultRejectedSourceMode: return @"REJECTED_SOURCE_MODE";
case MLNParticleHostFieldSubmitResultRejectedAdapter: return @"REJECTED_ADAPTER";
case MLNParticleHostFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE";
case MLNParticleHostFieldSubmitResultRejectedNonMonotonic: return @"REJECTED_NON_MONOTONIC";
case MLNParticleHostFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY";
case MLNParticleHostFieldSubmitResultAcceptedPendingInitialWindow: return @"ACCEPTED_PENDING_INITIAL_WINDOW";
case MLNParticleHostFieldSubmitResultInitialWindowReady: return @"INITIAL_WINDOW_READY";
case MLNParticleHostFieldSubmitResultAcceptedNextSnapshot: return @"ACCEPTED_NEXT_SNAPSHOT";
case MLNParticleHostFieldSubmitResultDuplicate: return @"DUPLICATE";
default: return @"UNKNOWN";
}
}
NSString *v4String(MLNParticleEncodedFieldSubmitResult result) {
switch (result) {
case MLNParticleEncodedFieldSubmitResultAcceptedInserted: return @"ACCEPTED_INSERTED";
case MLNParticleEncodedFieldSubmitResultAcceptedReplacedRevision: return @"ACCEPTED_REPLACED_REVISION";
case MLNParticleEncodedFieldSubmitResultDuplicate: return @"DUPLICATE";
case MLNParticleEncodedFieldSubmitResultRejectedDecode: return @"REJECTED_DECODE";
case MLNParticleEncodedFieldSubmitResultRejectedContract: return @"REJECTED_CONTRACT";
case MLNParticleEncodedFieldSubmitResultRejectedInvalidFieldKey: return @"REJECTED_INVALID_FIELD_KEY";
case MLNParticleEncodedFieldSubmitResultRejectedIdentityConflict: return @"REJECTED_IDENTITY_CONFLICT";
case MLNParticleEncodedFieldSubmitResultRejectedStaleRevision: return @"REJECTED_STALE_REVISION";
case MLNParticleEncodedFieldSubmitResultRejectedIncompatible: return @"REJECTED_INCOMPATIBLE";
case MLNParticleEncodedFieldSubmitResultRejectedCapacity: return @"REJECTED_CAPACITY";
default: return @"REJECTED_CONTRACT";
}
}
BOOL isWeatherObjectsNotReadyError(MLNParticleFieldRuntimeErrorCode code) {
switch (code) {
case MLNParticleFieldRuntimeErrorSourceMissing:
case MLNParticleFieldRuntimeErrorSourceTypeMismatch:
case MLNParticleFieldRuntimeErrorWeatherColorLayerMissing:
case MLNParticleFieldRuntimeErrorWeatherColorLayerTypeMismatch:
case MLNParticleFieldRuntimeErrorParticleLayerMissing:
case MLNParticleFieldRuntimeErrorParticleLayerTypeMismatch:
case MLNParticleFieldRuntimeErrorLayerSourceMismatch:
return YES;
default:
return NO;
}
}
NSString *attachErrorCode(NSError *error) {
switch (error.code) {
case MLNParticleFieldRuntimeErrorStyleNotReady: return @"STYLE_NOT_READY";
case MLNParticleFieldRuntimeErrorMapUnavailable: return @"MAP_NOT_READY";
default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"WEATHER_OBJECTS_NOT_READY" : @"NATIVE_REJECTED";
}
}
NSString *encodedFacadeErrorCode(NSError *error) {
switch (error.code) {
case MLNParticleFieldRuntimeErrorInvalidArgument: return @"INVALID_ARGUMENT";
case MLNParticleFieldRuntimeErrorDisposed: return @"RUNTIME_DISPOSED";
case MLNParticleFieldRuntimeErrorStyleNotReady: return @"SOURCE_MISSING";
default: return isWeatherObjectsNotReadyError((MLNParticleFieldRuntimeErrorCode)error.code) ? @"SOURCE_MISSING" : @"NATIVE_REJECTED";
}
}
NSDictionary *snapshot(MLNParticleFieldRuntime *runtime) {
NSString *state[] = {@"DISABLED", @"WAITING_FOR_FIELDS", @"READY", @"ERROR"};
NSString *playback[] = {@"PLAY", @"PAUSE"};
NSString *fps[] = {@"AUTO", @"FPS_30", @"FPS_60"};
return @{ @"state": state[runtime.state], @"playbackState": playback[runtime.playbackState], @"targetFps": fps[runtime.targetFps], @"disposed": @(runtime.disposed) };
}
}
@implementation WeatherRuntimeBridge
@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
RCT_EXPORT_MODULE(WeatherRuntimeBridge)
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params {
return std::make_shared<NativeWeatherRuntimeBridgeSpecJSI>(params);
}
// RN 0.85.2 injects this registry from the current Fabric/UIManager interop layer. It is not
// a Map registry: every call schedules a fresh UI block and validates the MLRN component class.
- (void)withMapForTag:(double)reactTag requireRuntime:(BOOL)requireRuntime resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(void (^)(MLNMapView *, MLNParticleFieldRuntime * _Nullable))action {
if (!validSafeInteger(reactTag, YES)) { reject(@"INVALID_ARGUMENT", @"reactTag must be a positive integer", nil); return; }
RCTViewRegistry *registry = _viewRegistry_DEPRECATED;
if (!registry) { reject(@"MAP_VIEW_NOT_FOUND", @"Fabric UIManager view registry is unavailable", nil); return; }
[registry addUIBlock:^(RCTViewRegistry *currentRegistry) {
UIView *component = [currentRegistry viewForReactTag:@((NSInteger)reactTag)];
// MLRN's iOS pod intentionally marks its implementation headers private. Its verified
// Fabric seam is this component class + contentView; use runtime checks, not private imports.
if (![component isKindOfClass:NSClassFromString(@"MLRNMapViewComponentView")]) { reject(@"MAP_VIEW_NOT_FOUND", @"reactTag does not resolve to a current MLRNMapView", nil); return; }
UIView *content = [component valueForKey:@"contentView"];
if (![content isKindOfClass:NSClassFromString(@"MLRNMapView")] || ![content isKindOfClass:MLNMapView.class]) { reject(@"MAP_VIEW_NOT_FOUND", @"MLRN MapView content view is unavailable", nil); return; }
MLNMapView *map = (MLNMapView *)content;
MLNParticleFieldRuntime *runtime = map.particleFieldRuntime;
if (requireRuntime && (!runtime || runtime.disposed)) { reject(@"RUNTIME_DISPOSED", @"WeatherRuntime is not attached to this map", nil); return; }
action(map, runtime);
}];
}
- (NSData *)readPayload:(NSString *)reference error:(NSString **)message {
if (reference.length == 0) { *message = @"payload file reference is empty"; return nil; }
NSURL *url = nil;
if ([reference hasPrefix:@"file://"]) url = [NSURL URLWithString:reference];
else if ([reference hasPrefix:@"/"]) url = [NSURL fileURLWithPath:reference];
else { *message = @"payload file reference must use file:// or an absolute local path"; return nil; }
if (!url.isFileURL) { *message = @"payload file reference must be local"; return nil; }
NSNumber *regular = nil, *readable = nil, *size = nil;
if (![url getResourceValue:&regular forKey:NSURLIsRegularFileKey error:nil] || !regular.boolValue ||
![url getResourceValue:&readable forKey:NSURLIsReadableKey error:nil] || !readable.boolValue ||
![url getResourceValue:&size forKey:NSURLFileSizeKey error:nil] || size.unsignedLongLongValue == 0 || size.unsignedLongLongValue > MaxPayloadBytes) {
*message = @"payload file must be a readable regular file of 1..67108864 bytes"; return nil;
}
const unsigned long long expected = size.unsignedLongLongValue;
NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:nil];
NSNumber *after = nil;
[url getResourceValue:&after forKey:NSURLFileSizeKey error:nil];
if (!data || data.length != expected || after.unsignedLongLongValue != expected) { *message = @"payload file changed while being read"; return nil; }
return data;
}
- (MLNParticleFieldRuntimeConfiguration *)configuration {
return [[MLNParticleFieldRuntimeConfiguration alloc] initWithSourceIdentifier:@"rn-weather-fields" weatherColorLayerIdentifier:@"rn-weather-color" particleLayerIdentifier:@"rn-weather-particles"];
}
- (void)attach:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
NSError *error = nil; MLNParticleFieldRuntime *attached = [map replaceParticleFieldRuntimeWithConfiguration:self.configuration error:&error];
if (!attached) {
reject(attachErrorCode(error), error.localizedDescription ?: @"WeatherRuntime attach rejected", error); return;
}
resolve(snapshot(attached));
}];
}
- (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); }];
}
- (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 {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
map.retainedUnderlayEnabled = enabled;
resolve(@"ACCEPTED");
}];
}
- (void)setContinuousLayerRange:(double)reactTag startLayerId:(NSString *)startLayerId endLayerId:(NSString *)endLayerId resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
resolve([map setContinuousLayerRangeFromLayerIdentifier:startLayerId toLayerIdentifier:endLayerId] ? @"ACCEPTED" : @"REJECTED");
}];
}
- (void)getRetainedUnderlayDiagnostics:(double)reactTag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[self withMapForTag:reactTag requireRuntime:NO resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) {
MLNRetainedUnderlayDiagnostics *value = map.retainedUnderlayDiagnostics;
resolve(@{ @"enabled": @(value.enabled), @"build": @(value.build), @"reuse": @(value.reuse),
@"fallback": @(value.fallback), @"invalidation": @(value.invalidation),
@"lastReason": value.lastReason, @"rangeConfigured": @(value.rangeConfigured),
@"rangeValid": @(value.rangeValid), @"startLayerId": value.startLayerId,
@"endLayerId": value.endLayerId, @"startIndex": @(value.startIndex), @"endIndex": @(value.endIndex),
@"continuousBegin": @(value.continuousBegin), @"continuousEndExclusive": @(value.continuousEndExclusive),
@"bottomPresent": @(value.bottomPresent), @"bottomCacheState": value.bottomCacheState,
@"bottomBuild": @(value.bottomBuild),
@"bottomReuse": @(value.bottomReuse), @"bottomInvalidation": @(value.bottomInvalidation),
@"bottomLastInvalidationReason": value.bottomLastInvalidationReason,
@"bottomTextureCreate": @(value.bottomTextureCreate),
@"bottomCompositeCreate": @(value.bottomCompositeCreate),
@"bottomResourceRelease": @(value.bottomResourceRelease),
@"topPresent": @(value.topPresent), @"topCacheState": value.topCacheState,
@"topBuild": @(value.topBuild), @"topReuse": @(value.topReuse),
@"topInvalidation": @(value.topInvalidation),
@"topLastInvalidationReason": value.topLastInvalidationReason,
@"topTextureCreate": @(value.topTextureCreate),
@"topCompositeCreate": @(value.topCompositeCreate),
@"topResourceRelease": @(value.topResourceRelease),
@"rangeFallback": @(value.rangeFallback), @"fastPathEligible": @(value.fastPathEligible),
@"fastPathActive": @(value.fastPathActive), @"fastPathFallbackReason": value.fastPathFallbackReason,
@"fastPathHit": @(value.fastPathHit), @"fastPathMiss": @(value.fastPathMiss),
@"fullCpuFrame": @(value.fullCpuFrame) });
}];
}
- (void)command:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action {
[self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); resolve(error ? @"REJECTED" : commandString(result)); }];
}
- (void)encodedCommand:(double)tag resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, NSError **))action {
[self withMapForTag:tag requireRuntime:YES resolve:resolve reject:reject action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleFieldRuntimeCommandResult result = action(runtime, &error); if (error) reject(encodedFacadeErrorCode(error), error.localizedDescription, error); else resolve(commandString(result)); }];
}
- (void)clearWindFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self clearFields:t resolve:r reject:j]; }
- (void)clearFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x, NSError **e) { return [x clearFieldsWithError:e]; }]; }
- (void)setWeatherEnabled:(double)t enabled:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherEnabled:v error:e];}]; }
- (void)setWeatherColorVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setWeatherColorVisible:v error:e];}]; }
- (void)setParticleVisible:(double)t visible:(BOOL)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setParticleVisible:v error:e];}]; }
- (void)submitWindField:(double)t metadataJson:(NSString *)metadata payloadFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; }
NSString *message = nil; NSData *data = [self readPayload:uri error:&message]; if (!data) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; }
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitWindFieldWithMetadataJSON:metadata payload:data error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }];
}
- (void)submitField:(double)t metadataJson:(NSString *)metadata validityFileUri:(NSString *)validity scalarFileUri:(NSString *)scalar vectorFileUri:(NSString *)vector resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
if (!metadata.length || [metadata lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxMetadataBytes) { j(@"INVALID_ARGUMENT", @"metadataJson UTF-8 payload must be non-empty and at most 1048576 bytes", nil); return; }
NSString *message = nil; NSData *v = [self readPayload:validity error:&message]; NSData *s = scalar.length ? [self readPayload:scalar error:&message] : nil; NSData *p = vector.length ? [self readPayload:vector error:&message] : nil;
if (!v || (scalar.length && !s) || (vector.length && !p) || (!s && !p) || v.length + s.length + p.length > MaxPayloadBytes) { j(@"PAYLOAD_FILE_UNREADABLE", message ?: @"payloadRefs requires scalarFileUri or vectorFileUri and at most 67108864 bytes", nil); return; }
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleHostFieldSubmitResult result = [runtime submitFieldWithMetadataJSON:metadata scalar:s vector:p validity:v error:&error]; if (error) j(@"NATIVE_REJECTED", error.localizedDescription, error); else r(v3String(result)); }];
}
- (void)submitEncodedField:(double)t fieldKey:(NSString *)key fieldID:(NSString *)fieldID generation:(double)generation dataVersion:(double)dataVersion contractJson:(NSString *)contract qbinFileUri:(NSString *)uri resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
if (!validIdentifier(key) || !validIdentifier(fieldID) || !validSafeInteger(generation, YES) || !validSafeInteger(dataVersion, NO) || !contract.length || [contract lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > MaxContractBytes) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; }
NSError *jsonError = nil; id json = [NSJSONSerialization JSONObjectWithData:[contract dataUsingEncoding:NSUTF8StringEncoding] options:0 error:&jsonError];
if (![json isKindOfClass:NSDictionary.class] || !validIdentifier(((NSDictionary *)json)[@"profileID"])) { j(@"INVALID_ARGUMENT", @"invalid V4 encoded field control-plane argument", nil); return; }
NSString *message = nil; NSData *qbin = [self readPayload:uri error:&message]; if (!qbin) { j(@"PAYLOAD_FILE_UNREADABLE", message, nil); return; }
[self withMapForTag:t requireRuntime:YES resolve:r reject:j action:^(MLNMapView *map, MLNParticleFieldRuntime *runtime) { NSError *error = nil; MLNParticleEncodedFieldSubmitResult result = [runtime submitEncodedFieldWithFieldKey:key fieldID:fieldID generation:(uint64_t)generation dataVersion:(uint64_t)dataVersion contractJSON:contract qbin:qbin error:&error]; if (error) j(encodedFacadeErrorCode(error), error.localizedDescription, error); else r(v4String(result)); }];
}
- (void)setEncodedColorBinding:(double)t fieldKey:(NSString *)key semanticID:(double)semantic groupID:(double)group expectedValueClass:(NSString *)valueClass expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
NSInteger value = [valueClass isEqual:@"SCALAR"] ? 0 : [valueClass isEqual:@"VELOCITY_COMPONENT"] ? 1 : [valueClass isEqual:@"DIRECTION_ANGLE"] ? 2 : -1;
if (!validIdentifier(key) || !validUInt16(semantic, YES) || !validUInt16(group, NO) || !validUInt16(unit, NO) || value < 0) { j(@"INVALID_ARGUMENT", @"invalid encoded color binding selector", nil); return; }
[self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedColorBindingWithFieldKey:key semanticID:semantic groupID:group expectedValueClass:(MLNParticleFieldValueClass)value expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }];
}
- (void)setEncodedParticleBinding:(double)t fieldKey:(NSString *)key mode:(NSString *)mode uSemanticID:(double)us uGroupID:(double)ug vSemanticID:(double)vs vGroupID:(double)vg directionSemanticID:(double)ds directionGroupID:(double)dg expectedUnitID:(double)unit resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j {
BOOL inactiveU = us == 0 && ug == 0, inactiveV = vs == 0 && vg == 0, inactiveD = ds == 0 && dg == 0;
BOOL vector = [mode isEqual:@"VECTOR_COMPONENTS"] && validUInt16(us, YES) && validUInt16(ug, NO) && validUInt16(vs, YES) && validUInt16(vg, NO) && inactiveD;
BOOL direction = [mode isEqual:@"DIRECTION_ANGLE"] && inactiveU && inactiveV && validUInt16(ds, YES) && validUInt16(dg, NO);
if (!validIdentifier(key) || !validUInt16(unit, NO) || (!vector && !direction)) { j(@"INVALID_ARGUMENT", @"active encoded particle selectors must be uint16 with non-zero semanticID; inactive slots are exactly zero", nil); return; }
[self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){ return [x setEncodedParticleBindingWithFieldKey:key mode:(vector ? MLNParticleEncodedParticleBindingModeVectorComponents : MLNParticleEncodedParticleBindingModeDirectionAngle) uSemanticID:us uGroupID:ug vSemanticID:vs vGroupID:vg directionSemanticID:ds directionGroupID:dg expectedUnitID:unit error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected; }];
}
- (void)clearEncodedField:(double)t fieldKey:(NSString *)key resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validIdentifier(key)) { j(@"INVALID_ARGUMENT", @"invalid fieldKey", nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearEncodedFieldWithFieldKey:key error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
- (void)clearAllEncodedFields:(double)t resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x clearAllEncodedFieldsWithError:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
- (void)setEncodedForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"encoded forecast time must be safe integer",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastTime:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
- (void)setEncodedForecastPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!([v isEqual:@"PLAY"] || [v isEqual:@"PAUSE"])) { j(@"INVALID_ARGUMENT", @"invalid encoded playback",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackState:[v isEqual:@"PLAY"] error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
- (void)setEncodedForecastPlaybackSpeed:(double)t speed:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0 || v > FLT_MAX) { j(@"INVALID_ARGUMENT", @"invalid encoded playback speed",nil); return; } [self encodedCommand:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *x,NSError **e){return [x setEncodedForecastPlaybackSpeed:v error:e] ? MLNParticleFieldRuntimeCommandResultAccepted : MLNParticleFieldRuntimeCommandResultRejected;}]; }
- (void)setTargetFps:(double)t target:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimeTargetFps x = [v isEqual:@"AUTO"] ? MLNParticleFieldRuntimeTargetFpsAuto : [v isEqual:@"FPS_30"] ? MLNParticleFieldRuntimeTargetFps30 : [v isEqual:@"FPS_60"] ? MLNParticleFieldRuntimeTargetFps60 : (MLNParticleFieldRuntimeTargetFps)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setTargetFps:x error:e];}]; }
- (void)setForecastTime:(double)t timestampMs:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!validSafeInteger(v, NO)) { j(@"INVALID_ARGUMENT", @"forecast timestamp must be a non-negative epoch millisecond integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setForecastTime:v error:e];}]; }
- (void)setPlaybackState:(double)t playback:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldRuntimePlaybackState x = [v isEqual:@"PLAY"] ? MLNParticleFieldRuntimePlaybackStatePlay : [v isEqual:@"PAUSE"] ? MLNParticleFieldRuntimePlaybackStatePause : (MLNParticleFieldRuntimePlaybackState)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackState:x error:e];}]; }
- (void)setPlaybackSpeed:(double)t speed:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { float x = [v isEqual:@"HALF"] ? .5f : [v isEqual:@"NORMAL"] ? 1.f : [v isEqual:@"DOUBLE"] ? 2.f : -1.f; if (x < 0) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setPlaybackSpeed:x error:e];}]; }
- (void)setFieldValueSource:(double)t source:(NSString *)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { MLNParticleFieldValueSource x = [v isEqual:@"SCALAR"] ? MLNParticleFieldValueSourceScalar : [v isEqual:@"VECTOR_MAGNITUDE"] ? MLNParticleFieldValueSourceVectorMagnitude : (MLNParticleFieldValueSource)99; if (x == 99) { j(@"INVALID_ARGUMENT", @"unsupported enum value",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setFieldValueSource:x error:e];}]; }
- (void)setParticleAdvectionScale:(double)t scale:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v <= 0) { j(@"INVALID_ARGUMENT", @"advectionScale must be finite and > 0",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleAdvectionScale:v error:e];}]; }
- (void)floatCommand:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, NSError **))a { if (!isfinite(v)) { j(@"INVALID_ARGUMENT", @"value must be finite",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q, v, e);}]; }
- (void)setWeatherColorOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setWeatherColorOpacity:x error:e];}]; }
- (void)setParticleLineWidth:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLineWidth:x error:e];}]; }
- (void)setParticleLength:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleLength:x error:e];}]; }
- (void)setParticleOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setParticleOpacity:x error:e];}]; }
- (void)setTrailOpacity:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailOpacity:x error:e];}]; }
- (void)setTrailFade:(double)t value:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self floatCommand:t value:v resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float x,NSError **e){return [q setTrailFade:x error:e];}]; }
- (void)setParticleCount:(double)t count:(double)v resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { if (!isfinite(v) || v < 0 || v > 2147483647.0 || floor(v) != v) { j(@"INVALID_ARGUMENT", @"particle count must be a non-negative integer",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setParticleCount:v error:e];}]; }
- (void)colorCommand:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j action:(MLNParticleFieldRuntimeCommandResult (^)(MLNParticleFieldRuntime *, float, float, float, float, NSError **))a { if (!isfinite(red)||!isfinite(green)||!isfinite(blue)||!isfinite(alpha)||red<0||red>1||green<0||green>1||blue<0||blue>1||alpha<0||alpha>1) { j(@"INVALID_ARGUMENT", @"RGBA must be finite values in [0,1]",nil); return; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return a(q,red,green,blue,alpha,e);}]; }
- (void)setParticleColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setParticleColorRed:a green:b blue:c alpha:d error:e];}]; }
- (void)setTrailColor:(double)t red:(double)red green:(double)green blue:(double)blue alpha:(double)alpha resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { [self colorCommand:t red:red green:green blue:blue alpha:alpha resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,float a,float b,float c,float d,NSError **e){return [q setTrailColorRed:a green:b blue:c alpha:d error:e];}]; }
- (void)setWeatherColorStops:(double)t stops:(NSArray *)stops resolve:(RCTPromiseResolveBlock)r reject:(RCTPromiseRejectBlock)j { NSMutableArray *native = NSMutableArray.array; float previous = -INFINITY; for (id entry in stops) { if (![entry isKindOfClass:NSDictionary.class]) { j(@"INVALID_ARGUMENT", @"color stop must be an object",nil); return; } NSDictionary *d = entry; NSNumber *speed=d[@"speedMps"],*red=d[@"red"],*green=d[@"green"],*blue=d[@"blue"],*alpha=d[@"alpha"]; if (!speed||!red||!green||!blue||!alpha||!isfinite(speed.floatValue)||speed.floatValue<=previous||!isfinite(red.floatValue)||!isfinite(green.floatValue)||!isfinite(blue.floatValue)||!isfinite(alpha.floatValue)||red.floatValue<0||red.floatValue>1||green.floatValue<0||green.floatValue>1||blue.floatValue<0||blue.floatValue>1||alpha.floatValue<0||alpha.floatValue>1) { j(@"INVALID_ARGUMENT", @"stops require finite, strictly increasing speedMps and finite RGBA in [0,1]",nil); return; } previous=speed.floatValue; [native addObject:[[MLNParticleWeatherColorStop alloc] initWithValue:previous red:red.floatValue green:green.floatValue blue:blue.floatValue alpha:alpha.floatValue]]; } [self command:t resolve:r reject:j action:^MLNParticleFieldRuntimeCommandResult(MLNParticleFieldRuntime *q,NSError **e){return [q setWeatherColorStops:native error:e];}]; }
@end