@rafaelaugusto/jolt-native 0.1.0-beta.13 → 0.1.0-beta.15
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
|
@@ -6,8 +6,12 @@ Optional NativeScript plugin for `@rafaelaugusto/jolt/native`. Install this pack
|
|
|
6
6
|
npm install @rafaelaugusto/jolt @rafaelaugusto/jolt-native
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Includes compiled scalar/compound playback, MapKit and SceneKit sinks on iOS, and a MapLibre sink on Android. It does not install MapLibre; the app chooses its SDK version. Reference demo: MapLibre Android 11.8.0, NativeScript 9.1, iOS 26 / Android 16. Minimum NativeScript 8.9; native helper APIs require iOS 13+ / Android API 23+ (the app/runtime or SDK may require newer).
|
|
9
|
+
Includes compiled scalar/compound playback, MapKit and SceneKit pose/light/material sinks on iOS, and a MapLibre sink on Android. It does not install MapLibre; the app chooses its SDK version. Reference demo: MapLibre Android 11.8.0, NativeScript 9.1, iOS 26 / Android 16. Minimum NativeScript 8.9; native helper APIs require iOS 13+ / Android API 23+ (the app/runtime or SDK may require newer).
|
|
10
|
+
|
|
11
|
+
SceneKit appearance supports light intensity/color and material transparency/solid diffuse/emission colors; textures are rejected. See the [lighting guide](https://jolt.rafaelaugusto.dev/guide/scene-lighting) for interpolation and lifecycle limits.
|
|
10
12
|
|
|
11
13
|
See the [native properties guide](https://jolt.rafaelaugusto.dev/guide/native-properties) for capabilities, authoring, ownership and execution limits.
|
|
12
14
|
|
|
13
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
|
+
|
|
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.
|
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.15",
|
|
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.15",
|
|
18
18
|
"@nativescript/core": ">=8.9.0"
|
|
19
19
|
},
|
|
20
20
|
"nativescript": {
|
|
@@ -4,5 +4,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
4
4
|
+ (JoltPropertySink *)mapCoordinate:(NSObject *)annotation;
|
|
5
5
|
+ (JoltPropertySink *)mapCamera:(NSObject *)mapView;
|
|
6
6
|
+ (JoltPropertySink *)scenePose:(NSObject *)node;
|
|
7
|
+
+ (JoltPropertySink *)sceneFieldOfView:(NSObject *)camera;
|
|
8
|
+
/** Diagnostic counts for camera FOV sinks and writes; native camera targets are weak. */
|
|
9
|
+
+ (NSUInteger)sceneCameraSinkCount;
|
|
10
|
+
+ (NSUInteger)sceneCameraWriteCount;
|
|
11
|
+
+ (JoltPropertySink *)sceneLightIntensity:(NSObject *)light;
|
|
12
|
+
+ (JoltPropertySink *)sceneLightColor:(NSObject *)light;
|
|
13
|
+
+ (JoltPropertySink *)sceneMaterialTransparency:(NSObject *)material;
|
|
14
|
+
+ (JoltPropertySink *)sceneMaterialDiffuse:(NSObject *)material;
|
|
15
|
+
+ (JoltPropertySink *)sceneMaterialEmission:(NSObject *)material;
|
|
16
|
+
/** Diagnostic count of undisposed appearance sinks (native targets are weak). */
|
|
17
|
+
+ (NSUInteger)sceneAppearanceSinkCount;
|
|
18
|
+
+ (NSUInteger)sceneAppearanceWriteCount;
|
|
7
19
|
@end
|
|
8
20
|
NS_ASSUME_NONNULL_END
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#import "JoltReferenceProperties.h"
|
|
2
2
|
#import <MapKit/MapKit.h>
|
|
3
3
|
#import <SceneKit/SceneKit.h>
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
4
5
|
|
|
5
6
|
static double longitude(double value) { return fmod(fmod(value+180,360)+360,360)-180; }
|
|
6
7
|
@interface JoltMapCoordinateSink : JoltPropertySink @end
|
|
@@ -47,7 +48,183 @@ static double longitude(double value) { return fmod(fmod(value+180,360)+360,360)
|
|
|
47
48
|
[SCNTransaction commit];
|
|
48
49
|
}
|
|
49
50
|
@end
|
|
51
|
+
// A standalone SCNCamera has no presentation getter. Read its current model FOV,
|
|
52
|
+
// and make each Jolt write immediate, just as for the appearance adapters below.
|
|
53
|
+
static NSUInteger cameraSinks, cameraWrites;
|
|
54
|
+
static BOOL validFieldOfView(double value) { return isfinite(value) && value >= 1 && value <= 179; }
|
|
55
|
+
static BOOL automaticPerspective(SCNCamera *camera) {
|
|
56
|
+
if (!camera || camera.usesOrthographicProjection || !validFieldOfView(camera.fieldOfView)) return NO;
|
|
57
|
+
if (camera.projectionDirection != SCNCameraProjectionDirectionVertical && camera.projectionDirection != SCNCameraProjectionDirectionHorizontal) return NO;
|
|
58
|
+
// A custom projectionTransform ignores viewport size. Inspect two projections
|
|
59
|
+
// through the public API without mutating the target or allocating a second camera.
|
|
60
|
+
// Checking only one matrix would accept a custom matrix equal to that viewport's
|
|
61
|
+
// automatic projection, which a fieldOfView write would silently replace.
|
|
62
|
+
SCNMatrix4 square = [camera projectionTransformWithViewportSize:CGSizeMake(100, 100)];
|
|
63
|
+
SCNMatrix4 wide = [camera projectionTransformWithViewportSize:CGSizeMake(200, 100)];
|
|
64
|
+
return isfinite(square.m11) && isfinite(square.m22) && isfinite(wide.m11) && isfinite(wide.m22)
|
|
65
|
+
&& square.m11 > 0 && square.m22 > 0 && !SCNMatrix4EqualToMatrix4(square, wide);
|
|
66
|
+
}
|
|
67
|
+
@interface JoltSceneCameraSink : JoltPropertySink
|
|
68
|
+
@property(nonatomic) BOOL released;
|
|
69
|
+
+ (instancetype)camera:(NSObject *)target;
|
|
70
|
+
@end
|
|
71
|
+
@implementation JoltSceneCameraSink
|
|
72
|
+
+ (instancetype)camera:(NSObject *)target {
|
|
73
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
74
|
+
if (![target isKindOfClass:SCNCamera.class])
|
|
75
|
+
[NSException raise:NSInvalidArgumentException format:@"Jolt field of view requires an SCNCamera, not its SCNNode"];
|
|
76
|
+
if (!automaticPerspective((SCNCamera *)target))
|
|
77
|
+
[NSException raise:NSInvalidArgumentException format:@"Jolt field of view requires an automatic perspective camera with an angle in 1..179 degrees; orthographic and custom projection matrices are unsupported"];
|
|
78
|
+
JoltSceneCameraSink *sink = [[self alloc] initWithTarget:target dimensions:1];
|
|
79
|
+
cameraSinks++;
|
|
80
|
+
return sink;
|
|
81
|
+
}
|
|
82
|
+
- (BOOL)alive { return automaticPerspective((SCNCamera *)self.target); }
|
|
83
|
+
- (NSArray<NSNumber *> *)read {
|
|
84
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
85
|
+
if (!self.alive) [NSException raise:NSInvalidArgumentException format:@"Jolt camera is no longer a supported perspective target"];
|
|
86
|
+
return @[@([(SCNCamera *)self.target fieldOfView])];
|
|
87
|
+
}
|
|
88
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
89
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
90
|
+
if (!self.alive) [NSException raise:NSInvalidArgumentException format:@"Jolt will not overwrite an unsupported camera projection"];
|
|
91
|
+
if (values.count != 1 || !validFieldOfView(values[0].doubleValue))
|
|
92
|
+
[NSException raise:NSInvalidArgumentException format:@"Jolt field of view needs one finite angle in 1..179 degrees"];
|
|
93
|
+
[SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
|
|
94
|
+
@try {
|
|
95
|
+
[(SCNCamera *)self.target setFieldOfView:values[0].doubleValue];
|
|
96
|
+
cameraWrites++;
|
|
97
|
+
} @finally { [SCNTransaction commit]; }
|
|
98
|
+
}
|
|
99
|
+
- (void)dispose {
|
|
100
|
+
if (!self.released) { self.released = YES; cameraSinks--; }
|
|
101
|
+
[super dispose];
|
|
102
|
+
}
|
|
103
|
+
- (void)dealloc { if (!self.released) cameraSinks--; }
|
|
104
|
+
@end
|
|
105
|
+
|
|
106
|
+
// Appearance is read from the model: SceneKit exposes presentationNode, but no public
|
|
107
|
+
// presentation object for a standalone SCNLight/SCNMaterial. Our immediate writes keep
|
|
108
|
+
// the model current. Apps must remove competing animations before transferring control.
|
|
109
|
+
typedef NS_ENUM(NSUInteger, JoltSceneAppearanceKind) {
|
|
110
|
+
JoltSceneIntensity, JoltSceneLightColor, JoltSceneTransparency, JoltSceneDiffuse, JoltSceneEmission
|
|
111
|
+
};
|
|
112
|
+
static NSUInteger appearanceSinks, appearanceWrites;
|
|
113
|
+
static CGFloat unitComponent(CGFloat value) { return MAX(0, MIN(1, value)); }
|
|
114
|
+
static CGColorRef copySceneColor(id value) CF_RETURNS_RETAINED {
|
|
115
|
+
CGColorRef original = NULL;
|
|
116
|
+
if ([value isKindOfClass:UIColor.class])
|
|
117
|
+
original = [[value resolvedColorWithTraitCollection:UITraitCollection.currentTraitCollection] CGColor];
|
|
118
|
+
else if (value && CFGetTypeID((__bridge CFTypeRef)value) == CGColorGetTypeID())
|
|
119
|
+
original = (__bridge CGColorRef)value;
|
|
120
|
+
if (!original) return NULL;
|
|
121
|
+
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
122
|
+
CGColorRef converted = CGColorCreateCopyByMatchingToColorSpace(space, kCGRenderingIntentDefault, original, NULL);
|
|
123
|
+
CGColorSpaceRelease(space);
|
|
124
|
+
if (converted && (CGColorGetNumberOfComponents(converted) != 4 || CGColorSpaceGetModel(CGColorGetColorSpace(converted)) != kCGColorSpaceModelRGB)) {
|
|
125
|
+
CGColorRelease(converted); return NULL;
|
|
126
|
+
}
|
|
127
|
+
return converted;
|
|
128
|
+
}
|
|
129
|
+
@interface JoltSceneAppearanceSink : JoltPropertySink
|
|
130
|
+
@property(nonatomic) JoltSceneAppearanceKind kind;
|
|
131
|
+
@property(nonatomic) BOOL released;
|
|
132
|
+
+ (instancetype)target:(NSObject *)target kind:(JoltSceneAppearanceKind)kind;
|
|
133
|
+
@end
|
|
134
|
+
@implementation JoltSceneAppearanceSink
|
|
135
|
+
+ (instancetype)target:(NSObject *)target kind:(JoltSceneAppearanceKind)kind {
|
|
136
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
137
|
+
BOOL light = kind == JoltSceneIntensity || kind == JoltSceneLightColor;
|
|
138
|
+
if (![target isKindOfClass:light ? SCNLight.class : SCNMaterial.class])
|
|
139
|
+
[NSException raise:NSInvalidArgumentException format:@"Jolt SceneKit adapter requires %@", light ? @"SCNLight" : @"SCNMaterial"];
|
|
140
|
+
JoltSceneAppearanceSink *sink = [[self alloc] initWithTarget:target dimensions:(kind == JoltSceneIntensity || kind == JoltSceneTransparency) ? 1 : 4];
|
|
141
|
+
sink.kind = kind; appearanceSinks++;
|
|
142
|
+
if (!sink.alive) {
|
|
143
|
+
[sink dispose];
|
|
144
|
+
[NSException raise:NSInvalidArgumentException format:@"Jolt SceneKit adapter requires a valid scalar or solid UIColor/CGColor; textures, images, numbers, patterns and nil color contents are unsupported"];
|
|
145
|
+
}
|
|
146
|
+
return sink;
|
|
147
|
+
}
|
|
148
|
+
- (id)contents {
|
|
149
|
+
switch (self.kind) {
|
|
150
|
+
case JoltSceneLightColor: return [(SCNLight *)self.target color];
|
|
151
|
+
case JoltSceneDiffuse: return [(SCNMaterial *)self.target diffuse].contents;
|
|
152
|
+
case JoltSceneEmission: return [(SCNMaterial *)self.target emission].contents;
|
|
153
|
+
default: return nil;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
- (BOOL)alive {
|
|
157
|
+
if (!self.target) return NO;
|
|
158
|
+
if (self.kind == JoltSceneIntensity || self.kind == JoltSceneTransparency) {
|
|
159
|
+
double value = self.kind == JoltSceneIntensity ? [(SCNLight *)self.target intensity] : [(SCNMaterial *)self.target transparency];
|
|
160
|
+
return isfinite(value) && value >= 0 && (self.kind != JoltSceneTransparency || value <= 1);
|
|
161
|
+
}
|
|
162
|
+
CGColorRef color = copySceneColor(self.contents);
|
|
163
|
+
BOOL valid = color != NULL;
|
|
164
|
+
if (color) CGColorRelease(color);
|
|
165
|
+
return valid;
|
|
166
|
+
}
|
|
167
|
+
- (NSArray<NSNumber *> *)read {
|
|
168
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
169
|
+
if (!self.alive) [NSException raise:NSInvalidArgumentException format:@"Jolt SceneKit target or solid color contents are no longer valid"];
|
|
170
|
+
if (self.kind == JoltSceneIntensity) return @[@([(SCNLight *)self.target intensity])];
|
|
171
|
+
if (self.kind == JoltSceneTransparency) return @[@([(SCNMaterial *)self.target transparency])];
|
|
172
|
+
CGColorRef color = copySceneColor(self.contents);
|
|
173
|
+
const CGFloat *v = CGColorGetComponents(color);
|
|
174
|
+
CGFloat a = unitComponent(v[3]);
|
|
175
|
+
NSArray *result = @[@(unitComponent(v[0])*a), @(unitComponent(v[1])*a), @(unitComponent(v[2])*a), @(a)];
|
|
176
|
+
CGColorRelease(color);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
- (void)write:(NSArray<NSNumber *> *)values {
|
|
180
|
+
NSParameterAssert(NSThread.isMainThread);
|
|
181
|
+
if (!self.alive) [NSException raise:NSInvalidArgumentException format:@"Jolt will not overwrite unsupported SceneKit contents"];
|
|
182
|
+
if (values.count != self.dimensions) [NSException raise:NSInvalidArgumentException format:@"Invalid SceneKit component count"];
|
|
183
|
+
for (NSNumber *value in values)
|
|
184
|
+
if (!isfinite(value.doubleValue)) [NSException raise:NSInvalidArgumentException format:@"Nonfinite SceneKit component"];
|
|
185
|
+
if (self.dimensions == 1 && (values[0].doubleValue < 0 || (self.kind == JoltSceneTransparency && values[0].doubleValue > 1)))
|
|
186
|
+
[NSException raise:NSInvalidArgumentException format:@"SceneKit scalar is outside its range"];
|
|
187
|
+
UIColor *color = nil;
|
|
188
|
+
if (self.dimensions == 4) {
|
|
189
|
+
double a = values[3].doubleValue;
|
|
190
|
+
if (a < 0 || a > 1) [NSException raise:NSInvalidArgumentException format:@"Invalid SceneKit alpha"];
|
|
191
|
+
for (NSUInteger i=0; i<3; i++)
|
|
192
|
+
if (values[i].doubleValue < 0 || values[i].doubleValue > a + 1e-9)
|
|
193
|
+
[NSException raise:NSInvalidArgumentException format:@"Invalid premultiplied SceneKit color"];
|
|
194
|
+
color = [UIColor colorWithRed:a ? unitComponent(values[0].doubleValue/a) : 0
|
|
195
|
+
green:a ? unitComponent(values[1].doubleValue/a) : 0
|
|
196
|
+
blue:a ? unitComponent(values[2].doubleValue/a) : 0 alpha:a];
|
|
197
|
+
}
|
|
198
|
+
[SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
|
|
199
|
+
@try {
|
|
200
|
+
switch (self.kind) {
|
|
201
|
+
case JoltSceneIntensity: [(SCNLight *)self.target setIntensity:values[0].doubleValue]; break;
|
|
202
|
+
case JoltSceneTransparency: [(SCNMaterial *)self.target setTransparency:values[0].doubleValue]; break;
|
|
203
|
+
case JoltSceneLightColor: [(SCNLight *)self.target setColor:color]; break;
|
|
204
|
+
case JoltSceneDiffuse: [(SCNMaterial *)self.target diffuse].contents = color; break;
|
|
205
|
+
case JoltSceneEmission: [(SCNMaterial *)self.target emission].contents = color; break;
|
|
206
|
+
}
|
|
207
|
+
appearanceWrites++;
|
|
208
|
+
} @finally { [SCNTransaction commit]; }
|
|
209
|
+
}
|
|
210
|
+
- (void)dispose {
|
|
211
|
+
if (!self.released) { self.released = YES; appearanceSinks--; }
|
|
212
|
+
[super dispose];
|
|
213
|
+
}
|
|
214
|
+
- (void)dealloc { if (!self.released) appearanceSinks--; }
|
|
215
|
+
@end
|
|
216
|
+
|
|
50
217
|
@implementation JoltReferenceProperties
|
|
218
|
+
+ (JoltPropertySink *)sceneFieldOfView:(NSObject *)camera { return [JoltSceneCameraSink camera:camera]; }
|
|
219
|
+
+ (NSUInteger)sceneCameraSinkCount { return cameraSinks; }
|
|
220
|
+
+ (NSUInteger)sceneCameraWriteCount { return cameraWrites; }
|
|
221
|
+
+ (JoltPropertySink *)sceneLightIntensity:(NSObject *)light { return [JoltSceneAppearanceSink target:light kind:JoltSceneIntensity]; }
|
|
222
|
+
+ (JoltPropertySink *)sceneLightColor:(NSObject *)light { return [JoltSceneAppearanceSink target:light kind:JoltSceneLightColor]; }
|
|
223
|
+
+ (JoltPropertySink *)sceneMaterialTransparency:(NSObject *)material { return [JoltSceneAppearanceSink target:material kind:JoltSceneTransparency]; }
|
|
224
|
+
+ (JoltPropertySink *)sceneMaterialDiffuse:(NSObject *)material { return [JoltSceneAppearanceSink target:material kind:JoltSceneDiffuse]; }
|
|
225
|
+
+ (JoltPropertySink *)sceneMaterialEmission:(NSObject *)material { return [JoltSceneAppearanceSink target:material kind:JoltSceneEmission]; }
|
|
226
|
+
+ (NSUInteger)sceneAppearanceSinkCount { return appearanceSinks; }
|
|
227
|
+
+ (NSUInteger)sceneAppearanceWriteCount { return appearanceWrites; }
|
|
51
228
|
+ (JoltPropertySink *)mapCoordinate:(NSObject *)annotation {
|
|
52
229
|
NSParameterAssert([annotation conformsToProtocol:@protocol(MKAnnotation)] && [annotation respondsToSelector:@selector(setCoordinate:)]);
|
|
53
230
|
return [[JoltMapCoordinateSink alloc] initWithTarget:annotation dimensions:2];
|