@rafaelaugusto/jolt-native 0.0.0-stage → 0.1.0-beta.13

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rafael Augusto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # Jolt native property runtime
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Optional NativeScript plugin for `@rafaelaugusto/jolt/native`. Install this package alongside Jolt and rebuild the app. Core Jolt does not depend on this plugin.
4
+
5
+ ```sh
6
+ npm install @rafaelaugusto/jolt @rafaelaugusto/jolt-native
7
+ ```
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).
10
+
11
+ See the [native properties guide](https://jolt.rafaelaugusto.dev/guide/native-properties) for capabilities, authoring, ownership and execution limits.
12
+
13
+ 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.
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // NativeScript discovers this optional plugin through package dependencies.
2
+ export {}
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@rafaelaugusto/jolt-native",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.13",
4
+ "description": "Optional compiled property adapters for Jolt (MapKit, MapLibre and SceneKit).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.js",
8
+ "files": [
9
+ "index.js",
10
+ "platforms/ios",
11
+ "platforms/android/java",
12
+ "platforms/android/include.gradle",
13
+ "platforms/android/proguard-rules.pro"
14
+ ],
15
+ "sideEffects": false,
16
+ "peerDependencies": {
17
+ "@rafaelaugusto/jolt": ">=0.1.0-beta.13",
18
+ "@nativescript/core": ">=8.9.0"
19
+ },
20
+ "nativescript": {
21
+ "platforms": {
22
+ "ios": "8.9.0",
23
+ "android": "8.9.0"
24
+ }
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/rafaelaugustos/jolt.git",
32
+ "directory": "packages/native"
33
+ }
34
+ }
@@ -0,0 +1,6 @@
1
+ def dependency = nativescriptDependencies.find { it.name == '@rafaelaugusto/jolt-native' }
2
+ if (dependency != null) {
3
+ android.buildTypes.all {
4
+ proguardFile new File(rootDir, "${dependency.directory}/platforms/android/proguard-rules.pro")
5
+ }
6
+ }
@@ -0,0 +1,60 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import java.lang.reflect.Constructor;
4
+ import java.lang.reflect.Field;
5
+ import java.lang.reflect.Method;
6
+
7
+ /** Optional MapLibre integration: cached reflection avoids a core dependency on the SDK. */
8
+ public final class MapLibrePropertySink extends PropertySink {
9
+ private final boolean camera;
10
+ private final Constructor<?> coordinate, builder;
11
+ private final Method read, write, build, targetMethod, zoom, tilt, bearing, cameraUpdate;
12
+ private final Method latitude, longitude;
13
+ private final Field cameraTarget, cameraZoom, cameraTilt, cameraBearing;
14
+ public MapLibrePropertySink(Object target, boolean camera) {
15
+ super(target,camera?5:2); this.camera=camera;
16
+ try {
17
+ Class<?> latLng=Class.forName("org.maplibre.android.geometry.LatLng");
18
+ coordinate=latLng.getConstructor(double.class,double.class);
19
+ latitude=latLng.getMethod("getLatitude"); longitude=latLng.getMethod("getLongitude");
20
+ if(camera) {
21
+ Class<?> position=Class.forName("org.maplibre.android.camera.CameraPosition");
22
+ Class<?> b=Class.forName("org.maplibre.android.camera.CameraPosition$Builder");
23
+ Class<?> update=Class.forName("org.maplibre.android.camera.CameraUpdate");
24
+ builder=b.getConstructor(); build=b.getMethod("build");
25
+ targetMethod=b.getMethod("target",latLng); zoom=b.getMethod("zoom",double.class);
26
+ tilt=b.getMethod("tilt",double.class); bearing=b.getMethod("bearing",double.class);
27
+ cameraUpdate=Class.forName("org.maplibre.android.camera.CameraUpdateFactory").getMethod("newCameraPosition",position);
28
+ read=target.getClass().getMethod("getCameraPosition"); write=target.getClass().getMethod("moveCamera",update);
29
+ cameraTarget=position.getField("target"); cameraZoom=position.getField("zoom");
30
+ cameraTilt=position.getField("tilt"); cameraBearing=position.getField("bearing");
31
+ } else {
32
+ read=target.getClass().getMethod("getPosition"); write=target.getClass().getMethod("setPosition",latLng);
33
+ builder=null; build=targetMethod=zoom=tilt=bearing=cameraUpdate=null;
34
+ cameraTarget=cameraZoom=cameraTilt=cameraBearing=null;
35
+ }
36
+ } catch(ReflectiveOperationException e) { throw new IllegalArgumentException("MapLibre adapter needs a compatible MapLibreMap or Marker",e); }
37
+ }
38
+ public double[] read() {
39
+ Object object=target.get(); if(object==null) return new double[getDimensions()];
40
+ try {
41
+ Object value=read.invoke(object), p=camera?cameraTarget.get(value):value;
42
+ double lat=((Number)latitude.invoke(p)).doubleValue(), lon=((Number)longitude.invoke(p)).doubleValue();
43
+ return camera?new double[]{lat,lon,cameraZoom.getDouble(value),cameraTilt.getDouble(value),cameraBearing.getDouble(value)}:new double[]{lat,lon};
44
+ } catch(ReflectiveOperationException e) { throw new IllegalStateException(e); }
45
+ }
46
+ public void write(double[] values) {
47
+ Object object=target.get(); if(object==null) return;
48
+ try {
49
+ double lat=Math.max(-85.05112878,Math.min(85.05112878,values[0]));
50
+ double lon=((values[1]+180)%360+360)%360-180;
51
+ Object p=coordinate.newInstance(lat,lon);
52
+ if(!camera) write.invoke(object,p);
53
+ else {
54
+ Object b=builder.newInstance(); targetMethod.invoke(b,p);
55
+ zoom.invoke(b,Math.max(0,Math.min(24,values[2]))); tilt.invoke(b,Math.max(0,Math.min(60,values[3])));
56
+ bearing.invoke(b,values[4]); write.invoke(object,cameraUpdate.invoke(null,build.invoke(b)));
57
+ }
58
+ } catch(ReflectiveOperationException e) { throw new IllegalStateException(e); }
59
+ }
60
+ }
@@ -0,0 +1,85 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import android.animation.Animator;
4
+ import android.animation.AnimatorListenerAdapter;
5
+ import android.animation.ValueAnimator;
6
+ import android.view.animation.LinearInterpolator;
7
+
8
+ /** Native sampled compound-property playback. No JS animation-update listener. */
9
+ public final class PropertyPlayer {
10
+ public interface Listener { void onEnd(boolean completed); }
11
+ private PropertySink sink;
12
+ private Listener listener;
13
+ private final double[] times, values, finalValue;
14
+ private final double duration, delay;
15
+ private final int repeats, dimensions;
16
+ private final boolean reverse;
17
+ private ValueAnimator animator;
18
+ private boolean suppressed, stopped, counted;
19
+ private long frames;
20
+ private static int active;
21
+
22
+ public PropertyPlayer(PropertySink sink, double[] times, double[] values, double[] finalValue,
23
+ double duration, double delay, double repeat, boolean reverse, Listener listener) {
24
+ if (!sink.isCompiled() || times.length<2 || values.length!=times.length*sink.getDimensions())
25
+ throw new IllegalArgumentException("Expected compiled sink and matching keyframes");
26
+ this.sink=sink; this.times=times.clone(); this.values=values.clone(); this.finalValue=finalValue.clone();
27
+ this.duration=Math.max(1,duration); this.delay=delay; this.repeats=repeat<0?-1:(int)repeat;
28
+ this.reverse=reverse; this.listener=listener; this.dimensions=sink.getDimensions();
29
+ control(0,false);
30
+ }
31
+ public static int activeCount() { return active; }
32
+ public long getFrames() { return frames; }
33
+ private void count(boolean running) {
34
+ if(counted==running) return;
35
+ counted=running; active+=running?1:-1;
36
+ }
37
+ private void apply(double progress) {
38
+ if(stopped) return;
39
+ if(!sink.isAlive()) { end(false); return; }
40
+ int low=0, high=times.length-1;
41
+ while(low+1<high) { int mid=(low+high)/2; if(times[mid]<=progress) low=mid; else high=mid; }
42
+ double span=times[high]-times[low], t=span>0?Math.max(0,Math.min(1,(progress-times[low])/span)):1;
43
+ double[] result=new double[dimensions];
44
+ for(int i=0;i<dimensions;i++) result[i]=values[low*dimensions+i]+(values[high*dimensions+i]-values[low*dimensions+i])*t;
45
+ write(result);
46
+ }
47
+ private void write(double[] value) {
48
+ try { if(sink!=null && sink.isAlive()) { sink.write(value); frames++; } else end(false); }
49
+ catch(RuntimeException e) { android.util.Log.e("Jolt","Native property write failed",e); end(false); }
50
+ }
51
+ private void end(boolean completed) {
52
+ count(false);
53
+ if(listener!=null && !stopped) listener.onEnd(completed);
54
+ if(!completed) cancel();
55
+ }
56
+ public void control(double time, boolean paused) {
57
+ if(stopped) return;
58
+ suppressed=true;
59
+ if(animator!=null) { animator.removeAllUpdateListeners(); animator.removeAllListeners(); animator.cancel(); }
60
+ animator=ValueAnimator.ofFloat(0,1);
61
+ animator.setInterpolator(new LinearInterpolator()); animator.setDuration((long)duration);
62
+ animator.setStartDelay((long)Math.max(0,delay-time)); animator.setRepeatCount(repeats);
63
+ animator.setRepeatMode(reverse?ValueAnimator.REVERSE:ValueAnimator.RESTART);
64
+ animator.addUpdateListener(value -> { if(!suppressed) apply(((Number)value.getAnimatedValue()).doubleValue()); });
65
+ animator.addListener(new AnimatorListenerAdapter() {
66
+ @Override public void onAnimationEnd(Animator animation) {
67
+ if(!suppressed && !stopped) { write(finalValue); end(true); }
68
+ }
69
+ });
70
+ animator.start();
71
+ if(paused) animator.pause();
72
+ if(time>=delay) animator.setCurrentPlayTime((long)Math.max(0,time-delay));
73
+ suppressed=false;
74
+ double local=Math.max(0,time-delay), iteration=Math.floor(local/duration), progress=(local%duration)/duration;
75
+ if(reverse && iteration%2>=1) progress=1-progress;
76
+ if(repeats>=0 && time>=delay+duration*(repeats+1)) write(finalValue); else apply(progress);
77
+ count(!paused && !stopped && (repeats<0 || time<delay+duration*(repeats+1)));
78
+ }
79
+ public void cancel() {
80
+ if(stopped) return;
81
+ stopped=true; listener=null; count(false);
82
+ if(animator!=null) { animator.removeAllUpdateListeners(); animator.removeAllListeners(); animator.cancel(); animator=null; }
83
+ sink=null;
84
+ }
85
+ }
@@ -0,0 +1,48 @@
1
+ package org.rafaelaugusto.jolt;
2
+
3
+ import java.lang.ref.WeakReference;
4
+ import java.lang.reflect.Method;
5
+
6
+ /** Implement in compiled Java/Kotlin. Property writes and reads run on Android's UI thread. */
7
+ public abstract class PropertySink {
8
+ protected final WeakReference<Object> target;
9
+ private final int dimensions;
10
+ protected PropertySink(Object target, int dimensions) { this.target=new WeakReference<>(target); this.dimensions=dimensions; }
11
+ public final int getDimensions() { return dimensions; }
12
+ public boolean isAlive() { return target.get()!=null; }
13
+ public final boolean isCompiled() {
14
+ return !java.lang.reflect.Proxy.isProxyClass(getClass()) && !getClass().getName().startsWith("com.tns.gen.");
15
+ }
16
+ public abstract double[] read();
17
+ public abstract void write(double[] values);
18
+ public void dispose() { target.clear(); }
19
+
20
+ /** Numeric JavaBean property, with cached reflection and no JS setter. */
21
+ public static PropertySink scalar(Object target, String getter, String setter) {
22
+ return new Scalar(target, getter, setter);
23
+ }
24
+ private static final class Scalar extends PropertySink {
25
+ private final Method getter, setter;
26
+ private final boolean useFloat;
27
+ Scalar(Object target, String get, String set) {
28
+ super(target,1);
29
+ try {
30
+ getter=target.getClass().getMethod(get);
31
+ Class<?> type=getter.getReturnType();
32
+ if (type!=float.class && type!=double.class) throw new IllegalArgumentException("Scalar properties must return float or double");
33
+ setter=target.getClass().getMethod(set,type); useFloat=type==float.class;
34
+ } catch (ReflectiveOperationException e) { throw new IllegalArgumentException("Unsupported numeric native property",e); }
35
+ }
36
+ public double[] read() {
37
+ Object object=target.get(); if(object==null) return new double[]{0};
38
+ try { return new double[]{((Number)getter.invoke(object)).doubleValue()}; }
39
+ catch (ReflectiveOperationException e) { throw new IllegalStateException(e); }
40
+ }
41
+ public void write(double[] values) {
42
+ Object object=target.get(); if(object==null) return;
43
+ try {
44
+ if(useFloat) setter.invoke(object, (float)values[0]); else setter.invoke(object,values[0]);
45
+ } catch (ReflectiveOperationException e) { throw new IllegalStateException(e); }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,13 @@
1
+ -keep class org.rafaelaugusto.jolt.PropertySink { *; }
2
+ -keep class org.rafaelaugusto.jolt.PropertySink$* { *; }
3
+ -keep class org.rafaelaugusto.jolt.PropertyPlayer { *; }
4
+ -keep class org.rafaelaugusto.jolt.PropertyPlayer$* { *; }
5
+ -keep class org.rafaelaugusto.jolt.MapLibrePropertySink { *; }
6
+ # Optional MapLibre adapters cache these SDK methods/fields by name.
7
+ -keep class org.maplibre.android.geometry.LatLng { *; }
8
+ -keep class org.maplibre.android.camera.CameraPosition { *; }
9
+ -keep class org.maplibre.android.camera.CameraPosition$Builder { *; }
10
+ -keep class org.maplibre.android.camera.CameraUpdate { *; }
11
+ -keep class org.maplibre.android.camera.CameraUpdateFactory { *; }
12
+ -keepclassmembers class org.maplibre.android.maps.MapLibreMap { public *** getCameraPosition(...); public void moveCamera(...); }
13
+ -keepclassmembers class org.maplibre.android.annotations.Marker { public *** getPosition(...); public void setPosition(...); }
@@ -0,0 +1 @@
1
+ OTHER_LDFLAGS = $(inherited) -framework MapKit -framework SceneKit
@@ -0,0 +1,28 @@
1
+ #import <Foundation/Foundation.h>
2
+ NS_ASSUME_NONNULL_BEGIN
3
+
4
+ /** Subclass in compiled Objective-C/Swift. Targets are weak; writes run on the main thread. */
5
+ @interface JoltPropertySink : NSObject
6
+ - (instancetype)initWithTarget:(NSObject *)target dimensions:(NSUInteger)dimensions;
7
+ + (instancetype)scalarTarget:(NSObject *)target keyPath:(NSString *)keyPath;
8
+ @property(nonatomic, weak, readonly, nullable) NSObject *target;
9
+ @property(nonatomic, readonly) NSUInteger dimensions;
10
+ @property(nonatomic, readonly) BOOL alive;
11
+ @property(nonatomic, readonly) BOOL compiled;
12
+ - (NSArray<NSNumber *> *)read;
13
+ - (void)write:(NSArray<NSNumber *> *)values;
14
+ - (void)dispose;
15
+ @end
16
+
17
+ @interface JoltPropertyPlayer : NSObject
18
+ + (instancetype)startSink:(JoltPropertySink *)sink times:(NSArray<NSNumber *> *)times
19
+ values:(NSArray<NSNumber *> *)values finalValue:(NSArray<NSNumber *> *)finalValue
20
+ duration:(double)duration delay:(double)delay repeat:(double)repeat reverse:(BOOL)reverse
21
+ completion:(void (^)(BOOL))completion;
22
+ @property(nonatomic, readonly) double elapsed;
23
+ @property(nonatomic, readonly) NSUInteger frames;
24
+ + (NSUInteger)activeCount;
25
+ - (void)control:(double)elapsed paused:(BOOL)paused;
26
+ - (void)cancel;
27
+ @end
28
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,130 @@
1
+ #import "JoltPropertyPlayer.h"
2
+ #import <QuartzCore/QuartzCore.h>
3
+ #import <UIKit/UIKit.h>
4
+ #import <objc/runtime.h>
5
+
6
+ @interface JoltPropertySink ()
7
+ @property(nonatomic, weak, readwrite) NSObject *target;
8
+ @property(nonatomic, readwrite) NSUInteger dimensions;
9
+ @end
10
+ @interface JoltScalarSink : JoltPropertySink
11
+ @property(nonatomic, copy) NSString *keyPath;
12
+ + (instancetype)target:(NSObject *)target keyPath:(NSString *)keyPath;
13
+ @end
14
+ @implementation JoltPropertySink
15
+ - (instancetype)initWithTarget:(NSObject *)target dimensions:(NSUInteger)dimensions {
16
+ if ((self = [super init])) { _target = target; _dimensions = dimensions; }
17
+ return self;
18
+ }
19
+ + (instancetype)scalarTarget:(NSObject *)target keyPath:(NSString *)keyPath { return [JoltScalarSink target:target keyPath:keyPath]; }
20
+ - (BOOL)alive { return self.target != nil; }
21
+ - (BOOL)compiled { return class_getImageName(self.class) != NULL; }
22
+ - (NSArray<NSNumber *> *)read { [self doesNotRecognizeSelector:_cmd]; return @[]; }
23
+ - (void)write:(NSArray<NSNumber *> *)values { [self doesNotRecognizeSelector:_cmd]; }
24
+ - (void)dispose { self.target = nil; }
25
+ @end
26
+
27
+ @implementation JoltScalarSink
28
+ + (instancetype)target:(NSObject *)target keyPath:(NSString *)keyPath {
29
+ JoltScalarSink *sink = [[self alloc] initWithTarget:target dimensions:1]; sink.keyPath = keyPath;
30
+ NSAssert([[target valueForKeyPath:keyPath] isKindOfClass:NSNumber.class], @"Expected a numeric native property");
31
+ return sink;
32
+ }
33
+ - (NSArray<NSNumber *> *)read { return @[[self.target valueForKeyPath:self.keyPath] ?: @0]; }
34
+ - (void)write:(NSArray<NSNumber *> *)values {
35
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
36
+ [self.target setValue:values[0] forKeyPath:self.keyPath];
37
+ [CATransaction commit];
38
+ }
39
+ @end
40
+
41
+ @interface JoltPropertyPlayer ()
42
+ @property(nonatomic, strong, nullable) JoltPropertySink *sink;
43
+ @property(nonatomic, copy) NSArray<NSNumber *> *times, *values, *finalValue;
44
+ @property(nonatomic) double duration, delay, repeats, anchor, offset;
45
+ @property(nonatomic) BOOL reverse, paused, stopped, ended;
46
+ @property(nonatomic, readwrite) NSUInteger frames;
47
+ @property(nonatomic, copy, nullable) void (^completion)(BOOL);
48
+ @end
49
+ @implementation JoltPropertyPlayer
50
+ static NSMutableSet<JoltPropertyPlayer *> *playing;
51
+ static CADisplayLink *displayLink;
52
+ + (void)initialize { if (self == JoltPropertyPlayer.class) playing = [NSMutableSet new]; }
53
+ + (void)schedule {
54
+ if (playing.count && !displayLink) {
55
+ displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
56
+ if (@available(iOS 15.0, *)) {
57
+ float rate = MIN(120, UIScreen.mainScreen.maximumFramesPerSecond);
58
+ displayLink.preferredFrameRateRange = CAFrameRateRangeMake(MIN(80, rate), rate, rate);
59
+ }
60
+ [displayLink addToRunLoop:NSRunLoop.mainRunLoop forMode:NSRunLoopCommonModes];
61
+ } else if (!playing.count) { [displayLink invalidate]; displayLink = nil; }
62
+ }
63
+ + (void)tick:(CADisplayLink *)frame {
64
+ for (JoltPropertyPlayer *player in playing.allObjects) {
65
+ if (!player.sink.alive) { [player end:NO]; continue; }
66
+ [player apply:player.elapsed];
67
+ if (isfinite(player.repeats) && player.elapsed >= player.delay + player.duration * (player.repeats + 1)) [player end:YES];
68
+ }
69
+ }
70
+ + (instancetype)startSink:(JoltPropertySink *)sink times:(NSArray<NSNumber *> *)times
71
+ values:(NSArray<NSNumber *> *)values finalValue:(NSArray<NSNumber *> *)finalValue
72
+ duration:(double)duration delay:(double)delay repeat:(double)repeat reverse:(BOOL)reverse
73
+ completion:(void (^)(BOOL))completion {
74
+ NSParameterAssert(NSThread.isMainThread);
75
+ NSParameterAssert(sink.compiled && sink.dimensions && values.count == times.count * sink.dimensions && finalValue.count == sink.dimensions);
76
+ JoltPropertyPlayer *player = [self new];
77
+ player.sink = sink; player.times = times; player.values = values; player.finalValue = finalValue;
78
+ player.duration = MAX(0.001, duration); player.delay = delay; player.repeats = repeat; player.reverse = reverse;
79
+ player.anchor = CACurrentMediaTime() * 1000; player.completion = completion;
80
+ [player apply:0];
81
+ if (!player.ended) { [playing addObject:player]; [self schedule]; }
82
+ return player;
83
+ }
84
+ - (double)elapsed { return self.offset + (self.paused ? 0 : CACurrentMediaTime() * 1000 - self.anchor); }
85
+ - (void)apply:(double)elapsed {
86
+ if (self.stopped || !self.sink.alive) return;
87
+ @try {
88
+ if (isfinite(self.repeats) && elapsed >= self.delay + self.duration * (self.repeats + 1)) {
89
+ [self.sink write:self.finalValue]; self.frames++; return;
90
+ }
91
+ double time = MAX(0, elapsed - self.delay), iteration = floor(time / self.duration);
92
+ double progress = fmod(time, self.duration) / self.duration;
93
+ if (self.reverse && fmod(iteration, 2) >= 1) progress = 1 - progress;
94
+ NSUInteger low = 0, high = self.times.count - 1;
95
+ while (low + 1 < high) {
96
+ NSUInteger mid = (low + high) / 2;
97
+ if (self.times[mid].doubleValue <= progress) low = mid; else high = mid;
98
+ }
99
+ double start = self.times[low].doubleValue, finish = self.times[high].doubleValue;
100
+ double t = finish > start ? MIN(1, MAX(0, (progress-start)/(finish-start))) : 1;
101
+ NSUInteger n = self.sink.dimensions;
102
+ NSMutableArray *value = [NSMutableArray arrayWithCapacity:n];
103
+ for (NSUInteger i=0; i<n; i++) {
104
+ double a=self.values[low*n+i].doubleValue, b=self.values[high*n+i].doubleValue;
105
+ [value addObject:@(a+(b-a)*t)];
106
+ }
107
+ [self.sink write:value]; self.frames++;
108
+ } @catch (NSException *exception) { NSLog(@"Jolt native property failed: %@", exception.reason); [self end:NO]; }
109
+ }
110
+ - (void)control:(double)elapsed paused:(BOOL)paused {
111
+ if (self.stopped) return;
112
+ self.offset=elapsed; self.anchor=CACurrentMediaTime()*1000; self.paused=paused; self.ended=NO;
113
+ [self apply:elapsed];
114
+ if (paused || self.ended) [playing removeObject:self]; else [playing addObject:self];
115
+ [JoltPropertyPlayer schedule];
116
+ }
117
+ - (void)end:(BOOL)completed {
118
+ if (self.ended || self.stopped) return;
119
+ self.ended=YES;
120
+ [playing removeObject:self]; [JoltPropertyPlayer schedule];
121
+ if (self.completion) self.completion(completed);
122
+ }
123
+ - (void)cancel {
124
+ if (self.stopped) return;
125
+ self.stopped=YES; self.completion=nil;
126
+ [playing removeObject:self]; self.sink=nil;
127
+ [JoltPropertyPlayer schedule];
128
+ }
129
+ + (NSUInteger)activeCount { return playing.count; }
130
+ @end
@@ -0,0 +1,8 @@
1
+ #import "JoltPropertyPlayer.h"
2
+ NS_ASSUME_NONNULL_BEGIN
3
+ @interface JoltReferenceProperties : NSObject
4
+ + (JoltPropertySink *)mapCoordinate:(NSObject *)annotation;
5
+ + (JoltPropertySink *)mapCamera:(NSObject *)mapView;
6
+ + (JoltPropertySink *)scenePose:(NSObject *)node;
7
+ @end
8
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,63 @@
1
+ #import "JoltReferenceProperties.h"
2
+ #import <MapKit/MapKit.h>
3
+ #import <SceneKit/SceneKit.h>
4
+
5
+ static double longitude(double value) { return fmod(fmod(value+180,360)+360,360)-180; }
6
+ @interface JoltMapCoordinateSink : JoltPropertySink @end
7
+ @implementation JoltMapCoordinateSink
8
+ - (NSArray<NSNumber *> *)read {
9
+ CLLocationCoordinate2D p=[(id<MKAnnotation>)self.target coordinate];
10
+ return @[@(p.latitude),@(p.longitude)];
11
+ }
12
+ - (void)write:(NSArray<NSNumber *> *)values {
13
+ id<MKAnnotation> target=(id)self.target;
14
+ if(target) target.coordinate=CLLocationCoordinate2DMake(MAX(-90,MIN(90,values[0].doubleValue)),longitude(values[1].doubleValue));
15
+ }
16
+ @end
17
+ @interface JoltMapCameraSink : JoltPropertySink @end
18
+ @implementation JoltMapCameraSink
19
+ - (NSArray<NSNumber *> *)read {
20
+ MKMapCamera *camera=[(MKMapView *)self.target camera];
21
+ return @[@(camera.centerCoordinate.latitude),@(camera.centerCoordinate.longitude),@(camera.altitude),@(camera.pitch),@(camera.heading)];
22
+ }
23
+ - (void)write:(NSArray<NSNumber *> *)values {
24
+ MKMapView *map=(id)self.target;
25
+ if(!map) return;
26
+ MKMapCamera *camera=[map.camera copy];
27
+ camera.centerCoordinate=CLLocationCoordinate2DMake(MAX(-90,MIN(90,values[0].doubleValue)),longitude(values[1].doubleValue));
28
+ camera.altitude=MAX(1,values[2].doubleValue); camera.pitch=MAX(0,MIN(80,values[3].doubleValue)); camera.heading=values[4].doubleValue;
29
+ [map setCamera:camera animated:NO];
30
+ }
31
+ @end
32
+ @interface JoltScenePoseSink : JoltPropertySink @end
33
+ @implementation JoltScenePoseSink
34
+ - (NSArray<NSNumber *> *)read {
35
+ SCNNode *node=(id)self.target;
36
+ SCNNode *presented=node.presentationNode ?: node;
37
+ SCNVector3 p=presented.position; SCNQuaternion q=presented.orientation;
38
+ return @[@(p.x),@(p.y),@(p.z),@(q.x),@(q.y),@(q.z),@(q.w)];
39
+ }
40
+ - (void)write:(NSArray<NSNumber *> *)values {
41
+ SCNNode *node=(id)self.target; if(!node) return;
42
+ [SCNTransaction begin]; [SCNTransaction setDisableActions:YES];
43
+ node.position=SCNVector3Make(values[0].floatValue,values[1].floatValue,values[2].floatValue);
44
+ double x=values[3].doubleValue,y=values[4].doubleValue,z=values[5].doubleValue,w=values[6].doubleValue;
45
+ double norm=sqrt(x*x+y*y+z*z+w*w);
46
+ node.orientation=norm>0?SCNVector4Make(x/norm,y/norm,z/norm,w/norm):SCNVector4Make(0,0,0,1);
47
+ [SCNTransaction commit];
48
+ }
49
+ @end
50
+ @implementation JoltReferenceProperties
51
+ + (JoltPropertySink *)mapCoordinate:(NSObject *)annotation {
52
+ NSParameterAssert([annotation conformsToProtocol:@protocol(MKAnnotation)] && [annotation respondsToSelector:@selector(setCoordinate:)]);
53
+ return [[JoltMapCoordinateSink alloc] initWithTarget:annotation dimensions:2];
54
+ }
55
+ + (JoltPropertySink *)mapCamera:(NSObject *)mapView {
56
+ NSParameterAssert([mapView isKindOfClass:MKMapView.class]);
57
+ return [[JoltMapCameraSink alloc] initWithTarget:mapView dimensions:5];
58
+ }
59
+ + (JoltPropertySink *)scenePose:(NSObject *)node {
60
+ NSParameterAssert([node isKindOfClass:SCNNode.class]);
61
+ return [[JoltScenePoseSink alloc] initWithTarget:node dimensions:7];
62
+ }
63
+ @end
@@ -0,0 +1,5 @@
1
+ module JoltNativeProperties {
2
+ header "JoltPropertyPlayer.h"
3
+ header "JoltReferenceProperties.h"
4
+ export *
5
+ }