@rafaelaugusto/jolt-native 0.1.0-beta.15 → 0.1.0-beta.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -15,3 +15,5 @@ See the [native properties guide](https://jolt.rafaelaugusto.dev/guide/native-pr
|
|
|
15
15
|
Release maintainers: publish this optional package together with compatible core releases (`npm publish` from this directory). Build and pack the core at the repository root. The demo uses local file dependencies and never needs the package published for validation.
|
|
16
16
|
|
|
17
17
|
SceneKit perspective camera field-of-view bindings use `sceneKitFieldOfView(camera)` from the core `/native` entry. See the [camera guide](https://jolt.rafaelaugusto.dev/guide/scene-camera) for projection, lens ownership and lifecycle limits. Available since beta.15. Install matching core/native versions and rebuild the native app to include this adapter.
|
|
18
|
+
|
|
19
|
+
Available since beta.17: `sceneKitEnvironmentIntensity(scene)` and `sceneKitUniform(target, name, options)` add native environment and literal Metal shader-modifier inputs on iOS 13+. They require the new compiled runtime and a native app rebuild. The [environment/shader guide](https://jolt.rafaelaugusto.dev/guide/scene-shaders) documents float/vector schemas, ranges, ownership, Reduced Motion and unsupported live sources/SCNProgram buffers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafaelaugusto/jolt-native",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.17",
|
|
4
4
|
"description": "Optional compiled property adapters for Jolt (MapKit, MapLibre and SceneKit).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"peerDependencies": {
|
|
17
|
-
"@rafaelaugusto/jolt": ">=0.1.0-beta.
|
|
17
|
+
"@rafaelaugusto/jolt": ">=0.1.0-beta.17",
|
|
18
18
|
"@nativescript/core": ">=8.9.0"
|
|
19
19
|
},
|
|
20
20
|
"nativescript": {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#import "JoltPropertyPlayer.h"
|
|
2
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
3
|
+
@interface JoltSceneShaderProperties : NSObject
|
|
4
|
+
+ (nullable JoltPropertySink *)environment:(NSObject *)scene error:(NSError * _Nullable * _Nullable)error;
|
|
5
|
+
+ (nullable JoltPropertySink *)uniform:(NSObject *)target name:(NSString *)name configuration:(NSString *)configuration error:(NSError * _Nullable * _Nullable)error;
|
|
6
|
+
+ (NSUInteger)sinkCount;
|
|
7
|
+
+ (NSUInteger)writeCount;
|
|
8
|
+
@end
|
|
9
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#import "JoltSceneShaderProperties.h"
|
|
2
|
+
#import <SceneKit/SceneKit.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
#import <float.h>
|
|
5
|
+
|
|
6
|
+
static NSUInteger sinks, writes;
|
|
7
|
+
static void invalid(NSString *message) {
|
|
8
|
+
[NSException raise:NSInvalidArgumentException format:@"%@", message];
|
|
9
|
+
}
|
|
10
|
+
static NSError *failure(NSException *exception) {
|
|
11
|
+
return [NSError errorWithDomain:@"JoltSceneShader" code:1 userInfo:@{NSLocalizedDescriptionKey: exception.reason ?: @"Invalid SceneKit target"}];
|
|
12
|
+
}
|
|
13
|
+
@interface JoltSceneTrackedSink : JoltPropertySink
|
|
14
|
+
@property(nonatomic) BOOL released;
|
|
15
|
+
@end
|
|
16
|
+
@implementation JoltSceneTrackedSink
|
|
17
|
+
- (instancetype)initWithTarget:(NSObject *)target dimensions:(NSUInteger)dimensions {
|
|
18
|
+
if ((self = [super initWithTarget:target dimensions:dimensions])) sinks++;
|
|
19
|
+
return self;
|
|
20
|
+
}
|
|
21
|
+
- (void)dispose { if (!self.released) { self.released = YES; sinks--; } [super dispose]; }
|
|
22
|
+
- (void)dealloc { if (!self.released) sinks--; }
|
|
23
|
+
@end
|
|
24
|
+
|
|
25
|
+
@interface JoltEnvironmentSink : JoltSceneTrackedSink @end
|
|
26
|
+
@implementation JoltEnvironmentSink
|
|
27
|
+
- (BOOL)alive {
|
|
28
|
+
SCNScene *scene = (id)self.target;
|
|
29
|
+
return scene && isfinite(scene.lightingEnvironment.intensity) && scene.lightingEnvironment.intensity >= 0;
|
|
30
|
+
}
|
|
31
|
+
- (NSArray<NSNumber *> *)read {
|
|
32
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
33
|
+
if (!self.alive) invalid(@"SceneKit environment target or intensity is no longer valid");
|
|
34
|
+
return @[@([(SCNScene *)self.target lightingEnvironment].intensity)];
|
|
35
|
+
}
|
|
36
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
37
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
38
|
+
if (!self.alive || values.count != 1 || !isfinite(values[0].doubleValue) || values[0].doubleValue < 0)
|
|
39
|
+
invalid(@"Environment intensity needs one finite nonnegative multiplier");
|
|
40
|
+
[SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
|
|
41
|
+
@try { [(SCNScene *)self.target lightingEnvironment].intensity = values[0].doubleValue; writes++; }
|
|
42
|
+
@finally { [SCNTransaction commit]; }
|
|
43
|
+
}
|
|
44
|
+
@end
|
|
45
|
+
|
|
46
|
+
static NSString *withoutComments(NSString *source) {
|
|
47
|
+
NSRegularExpression *comments = [NSRegularExpression regularExpressionWithPattern:@"/\\*[\\s\\S]*?\\*/|//[^\\n]*" options:0 error:NULL];
|
|
48
|
+
return [comments stringByReplacingMatchesInString:source options:0 range:NSMakeRange(0, source.length) withTemplate:@" "];
|
|
49
|
+
}
|
|
50
|
+
static void validateDeclaration(NSObject<SCNShadable> *target, NSString *name, NSString *type) {
|
|
51
|
+
if (target.program) invalid(@"SCNProgram buffers are unsupported; use declared Metal shaderModifiers inputs");
|
|
52
|
+
if ([target respondsToSelector:NSSelectorFromString(name)] || [target respondsToSelector:NSSelectorFromString([name stringByAppendingString:@":"])])
|
|
53
|
+
invalid(@"A shader input must not shadow a native target property");
|
|
54
|
+
NSDictionary *modifiers = target.shaderModifiers;
|
|
55
|
+
if (!modifiers.count) invalid(@"Declare the uniform in shaderModifiers before creating its adapter");
|
|
56
|
+
BOOL found = NO;
|
|
57
|
+
NSString *pattern = [NSString stringWithFormat:@"(?m)^\\s*(float[234]?)\\s+%@\\s*;\\s*$", name];
|
|
58
|
+
NSRegularExpression *declaration = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:NULL];
|
|
59
|
+
NSRegularExpression *arguments = [NSRegularExpression regularExpressionWithPattern:@"#pragma[ \\t]+arguments\\b([\\s\\S]*?)(?=#pragma|\\z)" options:0 error:NULL];
|
|
60
|
+
NSRegularExpression *conditional = [NSRegularExpression regularExpressionWithPattern:@"(?m)^\\s*#\\s*(if|ifdef|ifndef|define|include)\\b" options:0 error:NULL];
|
|
61
|
+
for (NSString *raw in modifiers.allValues) {
|
|
62
|
+
if (![raw isKindOfClass:NSString.class]) invalid(@"Shader modifiers must contain source strings");
|
|
63
|
+
NSString *source = withoutComments(raw);
|
|
64
|
+
if ([conditional numberOfMatchesInString:source options:0 range:NSMakeRange(0, source.length)])
|
|
65
|
+
invalid(@"Conditional or macro-defined shader schemas are unsupported; declare literal Metal float inputs");
|
|
66
|
+
for (NSTextCheckingResult *section in [arguments matchesInString:source options:0 range:NSMakeRange(0, source.length)]) {
|
|
67
|
+
NSString *body = [source substringWithRange:[section rangeAtIndex:1]];
|
|
68
|
+
NSArray *matches = [declaration matchesInString:body options:0 range:NSMakeRange(0, body.length)];
|
|
69
|
+
if (matches.count > 1) invalid(@"A uniform declaration must not repeat within one shader stage");
|
|
70
|
+
for (NSTextCheckingResult *match in matches) {
|
|
71
|
+
if (![[body substringWithRange:[match rangeAtIndex:1]] isEqualToString:type]) invalid(@"Uniform declaration does not match the requested component type");
|
|
72
|
+
found = YES;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!found) invalid(@"Uniform name/type was not found as a literal declaration under #pragma arguments");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@interface JoltUniformSink : JoltSceneTrackedSink
|
|
80
|
+
@property(nonatomic, copy) NSString *name;
|
|
81
|
+
@property(nonatomic, copy) NSDictionary *modifiers;
|
|
82
|
+
@property(nonatomic) double low, high;
|
|
83
|
+
@property(nonatomic) BOOL invalidated;
|
|
84
|
+
- (NSArray<NSNumber *> *)components;
|
|
85
|
+
@end
|
|
86
|
+
@implementation JoltUniformSink
|
|
87
|
+
- (NSArray<NSNumber *> *)components {
|
|
88
|
+
id value = [self.target valueForKey:self.name];
|
|
89
|
+
NSArray *values;
|
|
90
|
+
if (self.dimensions == 1 && [value isKindOfClass:NSNumber.class]) values = @[value];
|
|
91
|
+
else if ([value isKindOfClass:NSValue.class]) {
|
|
92
|
+
// SceneKit boxes SCNVector3/4 as CGRect on some runtimes; its own accessors
|
|
93
|
+
// decode that canonical representation. The declared shader type owns arity.
|
|
94
|
+
const char *encoding = [value objCType];
|
|
95
|
+
if (self.dimensions == 2 && strcmp(encoding, @encode(CGPoint)) == 0) {
|
|
96
|
+
CGPoint p = [value CGPointValue]; values = @[@(p.x), @(p.y)];
|
|
97
|
+
} else if (self.dimensions == 3 && (strcmp(encoding, @encode(SCNVector3)) == 0 || strcmp(encoding, @encode(CGRect)) == 0)) {
|
|
98
|
+
SCNVector3 p = [value SCNVector3Value]; values = @[@(p.x), @(p.y), @(p.z)];
|
|
99
|
+
} else if (self.dimensions == 4 && (strcmp(encoding, @encode(SCNVector4)) == 0 || strcmp(encoding, @encode(CGRect)) == 0)) {
|
|
100
|
+
SCNVector4 p = [value SCNVector4Value]; values = @[@(p.x), @(p.y), @(p.z), @(p.w)];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (values.count != self.dimensions) invalid([NSString stringWithFormat:@"Initialize uniform %@ with %lu components using NSNumber / NSValue CGPoint / SCNVector3 / SCNVector4; got %@ (%s)", self.name, (unsigned long)self.dimensions, [value class], [value isKindOfClass:NSValue.class] ? [value objCType] : "nil"]);
|
|
104
|
+
NSMutableArray *normalized = [NSMutableArray arrayWithCapacity:self.dimensions];
|
|
105
|
+
for (NSNumber *number in values) {
|
|
106
|
+
double v = number.doubleValue;
|
|
107
|
+
double epsilon = self.dimensions == 1 ? 0 : 4 * FLT_EPSILON * MAX(1, fabs(v));
|
|
108
|
+
if (!isfinite(v) || v < self.low - epsilon || v > self.high + epsilon)
|
|
109
|
+
invalid(@"Shader component is outside the declared finite range");
|
|
110
|
+
[normalized addObject:@(MAX(self.low, MIN(self.high, v)))];
|
|
111
|
+
}
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
- (BOOL)alive {
|
|
115
|
+
if (self.invalidated || !self.target) return NO;
|
|
116
|
+
@try {
|
|
117
|
+
NSObject<SCNShadable> *target = (id)self.target;
|
|
118
|
+
if (target.program || ![target.shaderModifiers isEqualToDictionary:self.modifiers]) self.invalidated = YES;
|
|
119
|
+
if (!self.invalidated) [self components];
|
|
120
|
+
} @catch (NSException *exception) { self.invalidated = YES; }
|
|
121
|
+
return !self.invalidated;
|
|
122
|
+
}
|
|
123
|
+
- (NSArray<NSNumber *> *)read {
|
|
124
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
125
|
+
if (!self.alive) invalid(@"Shader target, source or initialized value changed; dispose and create a new adapter");
|
|
126
|
+
return [self components];
|
|
127
|
+
}
|
|
128
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
129
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
130
|
+
if (!self.alive || values.count != self.dimensions) invalid(@"Shader target or component count is invalid");
|
|
131
|
+
for (NSNumber *number in values)
|
|
132
|
+
if (!isfinite(number.doubleValue) || number.doubleValue < self.low || number.doubleValue > self.high)
|
|
133
|
+
invalid(@"Shader component is outside the declared finite range");
|
|
134
|
+
id value = values[0];
|
|
135
|
+
if (self.dimensions == 2) value = [NSValue valueWithCGPoint:CGPointMake(values[0].doubleValue, values[1].doubleValue)];
|
|
136
|
+
else if (self.dimensions == 3) value = [NSValue valueWithSCNVector3:SCNVector3Make(values[0].floatValue, values[1].floatValue, values[2].floatValue)];
|
|
137
|
+
else if (self.dimensions == 4) value = [NSValue valueWithSCNVector4:SCNVector4Make(values[0].floatValue, values[1].floatValue, values[2].floatValue, values[3].floatValue)];
|
|
138
|
+
// One boxed vector assignment prevents separately updated component states.
|
|
139
|
+
[SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
|
|
140
|
+
@try { [self.target setValue:value forKey:self.name]; writes++; }
|
|
141
|
+
@finally { [SCNTransaction commit]; }
|
|
142
|
+
}
|
|
143
|
+
@end
|
|
144
|
+
|
|
145
|
+
@implementation JoltSceneShaderProperties
|
|
146
|
+
+ (JoltPropertySink *)environment:(NSObject *)scene error:(NSError **)error {
|
|
147
|
+
@try {
|
|
148
|
+
if (!NSThread.isMainThread || ![scene isKindOfClass:SCNScene.class]) invalid(@"Environment intensity needs an SCNScene on the main thread");
|
|
149
|
+
double value = [(SCNScene *)scene lightingEnvironment].intensity;
|
|
150
|
+
if (!isfinite(value) || value < 0) invalid(@"Environment intensity must start finite and nonnegative");
|
|
151
|
+
return [[JoltEnvironmentSink alloc] initWithTarget:scene dimensions:1];
|
|
152
|
+
} @catch (NSException *exception) { if (error) *error = failure(exception); return nil; }
|
|
153
|
+
}
|
|
154
|
+
+ (JoltPropertySink *)uniform:(NSObject *)target name:(NSString *)name configuration:(NSString *)configuration error:(NSError **)error {
|
|
155
|
+
JoltUniformSink *sink = nil;
|
|
156
|
+
@try {
|
|
157
|
+
if (!NSThread.isMainThread || (![target isKindOfClass:SCNMaterial.class] && ![target isKindOfClass:SCNGeometry.class]))
|
|
158
|
+
invalid(@"Shader uniforms need an SCNMaterial or SCNGeometry on the main thread");
|
|
159
|
+
NSRegularExpression *identifier = [NSRegularExpression regularExpressionWithPattern:@"^[A-Za-z][A-Za-z0-9_]*$" options:0 error:NULL];
|
|
160
|
+
if (![name isKindOfClass:NSString.class] || [identifier numberOfMatchesInString:name options:0 range:NSMakeRange(0, name.length)] != 1
|
|
161
|
+
|| [name hasPrefix:@"u_"] || [name hasPrefix:@"a_"] || [name hasPrefix:@"v_"] || [name hasPrefix:@"scn_"])
|
|
162
|
+
invalid(@"Invalid or reserved shader uniform identifier");
|
|
163
|
+
NSDictionary *config = [NSJSONSerialization JSONObjectWithData:[configuration dataUsingEncoding:NSUTF8StringEncoding] options:0 error:NULL];
|
|
164
|
+
if (![config isKindOfClass:NSDictionary.class]) invalid(@"Invalid shader schema");
|
|
165
|
+
NSString *type = config[@"type"];
|
|
166
|
+
NSArray *types = @[@"float", @"float2", @"float3", @"float4"];
|
|
167
|
+
NSUInteger index = [types indexOfObject:type];
|
|
168
|
+
if (index == NSNotFound || ![config[@"low"] isKindOfClass:NSNumber.class] || ![config[@"high"] isKindOfClass:NSNumber.class]) invalid(@"Invalid shader type or range");
|
|
169
|
+
double low = [config[@"low"] doubleValue], high = [config[@"high"] doubleValue];
|
|
170
|
+
if (!isfinite(low) || !isfinite(high) || low >= high || fabs(low) > FLT_MAX || fabs(high) > FLT_MAX) invalid(@"Shader ranges must be ordered finite float limits");
|
|
171
|
+
validateDeclaration((id)target, name, type);
|
|
172
|
+
sink = [[JoltUniformSink alloc] initWithTarget:target dimensions:index+1];
|
|
173
|
+
sink.name = name; sink.low = low; sink.high = high;
|
|
174
|
+
sink.modifiers = [(id<SCNShadable>)target shaderModifiers];
|
|
175
|
+
[sink components];
|
|
176
|
+
return sink;
|
|
177
|
+
} @catch (NSException *exception) { [sink dispose]; if (error) *error = failure(exception); return nil; }
|
|
178
|
+
}
|
|
179
|
+
+ (NSUInteger)sinkCount { return sinks; }
|
|
180
|
+
+ (NSUInteger)writeCount { return writes; }
|
|
181
|
+
@end
|