react-native-magnetkit 0.0.0-stage → 0.1.0
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 +21 -0
- package/README.md +72 -2
- package/android/build.gradle +18 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetDipole.kt +76 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetModel.kt +189 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetSource.kt +598 -0
- package/android/src/main/java/dev/libraries/magnetkit/MfxlNet.kt +92 -0
- package/android/src/main/java/dev/libraries/magnetkit/Vec3.kt +30 -0
- package/android/src/main/java/dev/libraries/magnetkit/rn/MagnetKitModule.kt +172 -0
- package/app.plugin.js +1 -0
- package/build/MagnetCalibration.d.ts +13 -0
- package/build/MagnetCalibration.js +58 -0
- package/build/MagnetKitModule.d.ts +52 -0
- package/build/MagnetKitModule.js +2 -0
- package/build/MagneticView.d.ts +15 -0
- package/build/MagneticView.js +129 -0
- package/build/core/calibration-core.d.ts +43 -0
- package/build/core/calibration-core.js +84 -0
- package/build/core/field.d.ts +64 -0
- package/build/core/field.js +341 -0
- package/build/core/fit.d.ts +13 -0
- package/build/core/fit.js +50 -0
- package/build/core/types.d.ts +37 -0
- package/build/core/types.js +2 -0
- package/build/hooks.d.ts +7 -0
- package/build/hooks.js +29 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +7 -0
- package/build/magnet.d.ts +29 -0
- package/build/magnet.js +66 -0
- package/expo-module.config.json +9 -0
- package/ios/MagnetKit/MFXLNet.swift +89 -0
- package/ios/MagnetKit/MagnetLocator.swift +252 -0
- package/ios/MagnetKit/MagnetModel.swift +56 -0
- package/ios/MagnetKit/MagnetPresence.swift +81 -0
- package/ios/MagnetKit/MagnetSource.swift +967 -0
- package/ios/MagnetKit/MagnetTap.swift +53 -0
- package/ios/MagnetKitModule.swift +147 -0
- package/ios/MagnetKitRN.podspec +26 -0
- package/package.json +54 -4
- package/plugin/withMagnetKit.js +60 -0
- package/src/MagnetCalibration.tsx +87 -0
- package/src/MagnetKitModule.ts +42 -0
- package/src/MagneticView.tsx +153 -0
- package/src/core/calibration-core.ts +112 -0
- package/src/core/field.ts +295 -0
- package/src/core/fit.ts +55 -0
- package/src/core/types.ts +32 -0
- package/src/hooks.ts +32 -0
- package/src/index.ts +9 -0
- package/src/magnet.ts +77 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
package dev.libraries.magnetkit
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.hardware.Sensor
|
|
5
|
+
import android.hardware.SensorEvent
|
|
6
|
+
import android.hardware.SensorEventListener
|
|
7
|
+
import android.hardware.SensorManager
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import org.json.JSONArray
|
|
11
|
+
import kotlin.math.abs
|
|
12
|
+
import kotlin.math.acos
|
|
13
|
+
import kotlin.math.exp
|
|
14
|
+
import kotlin.math.hypot
|
|
15
|
+
import kotlin.math.ln
|
|
16
|
+
import kotlin.math.log10
|
|
17
|
+
import kotlin.math.max
|
|
18
|
+
import kotlin.math.min
|
|
19
|
+
import kotlin.math.pow
|
|
20
|
+
import kotlin.math.sqrt
|
|
21
|
+
|
|
22
|
+
/** How `MagnetConfig.locatorEnabled` finds the magnet. */
|
|
23
|
+
enum class MagnetLocatorMode { NETWORK, DIPOLE }
|
|
24
|
+
|
|
25
|
+
/** A quick magnet movement. */
|
|
26
|
+
sealed class MagnetGesture {
|
|
27
|
+
/** Pushed quickly toward the phone; approach speed, cm/s. */
|
|
28
|
+
data class Push(val speed: Double) : MagnetGesture()
|
|
29
|
+
/** Pulled quickly away; retreat speed, cm/s. */
|
|
30
|
+
data class PullAway(val speed: Double) : MagnetGesture()
|
|
31
|
+
/** An air tap: hovering over the glass, dipped toward the screen and back. `at`: the located point, dp, with the locator on. */
|
|
32
|
+
data class Tap(val at: Pt?) : MagnetGesture()
|
|
33
|
+
/** Swept across the screen (needs the locator); unit direction and speed, dp/s. */
|
|
34
|
+
data class Swipe(val direction: Pt, val speed: Double) : MagnetGesture()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** How a magnetometer reading becomes a magnet reading. Lengths on screen are dp. */
|
|
38
|
+
data class MagnetConfig(
|
|
39
|
+
/** Field change, µT, that reads as full pull. */
|
|
40
|
+
var fullScale: Double = 60.0,
|
|
41
|
+
/** Knee of the log scale, µT. */
|
|
42
|
+
var scale: Double = 10.0,
|
|
43
|
+
/** Change below this, µT, is noise. */
|
|
44
|
+
var deadzone: Double = 3.0,
|
|
45
|
+
/** Low-pass time constant on the reading, s. */
|
|
46
|
+
var smoothing: Double = 0.06,
|
|
47
|
+
/** Re-zero once the phone has settled in a new pose. */
|
|
48
|
+
var autoRecalibrate: Boolean = true,
|
|
49
|
+
var poseTolerance: Double = 10.0,
|
|
50
|
+
var settleSeconds: Double = 0.8,
|
|
51
|
+
/** Where the magnetometer sits, dp from the window's top left. */
|
|
52
|
+
var sensorPosition: Pt? = null,
|
|
53
|
+
/** The window's size, dp (for placing a magnet resting beside an edge). */
|
|
54
|
+
var screenSize: Pt? = null,
|
|
55
|
+
var distanceK: Double = 16.0,
|
|
56
|
+
/** dp per centimetre (160 dp per inch). */
|
|
57
|
+
var pointsPerCm: Double = 160.0 / 2.54,
|
|
58
|
+
var driftSeconds: Double = 2.0,
|
|
59
|
+
var driftThreshold: Double? = null,
|
|
60
|
+
var steadyRezero: Boolean = true,
|
|
61
|
+
/** Above this, µT, the magnet is close enough to the sensor to magnetise the phone. */
|
|
62
|
+
var tooCloseStrength: Double = 2000.0,
|
|
63
|
+
var presenceModel: Boolean = true,
|
|
64
|
+
var tapModel: Boolean = true,
|
|
65
|
+
var locatorEnabled: Boolean = false,
|
|
66
|
+
/** The magnet's dipole constant, µT·cm³, if known. */
|
|
67
|
+
var magnetStrength: Double? = null,
|
|
68
|
+
/** +1 north toward the user, −1 south, null unknown. */
|
|
69
|
+
var magnetFace: Int? = null,
|
|
70
|
+
var locatorMode: MagnetLocatorMode = MagnetLocatorMode.NETWORK,
|
|
71
|
+
var dipoleMaxHeight: Double? = 4.0,
|
|
72
|
+
var deskPrior: Boolean = true,
|
|
73
|
+
var gesturePushRate: Double = 5.0,
|
|
74
|
+
var gesturePullRate: Double = 5.0,
|
|
75
|
+
var gestureTapWindow: Double = 0.45,
|
|
76
|
+
var gestureMinPull: Double = 0.2,
|
|
77
|
+
var gestureSwipeSpeed: Double = 1500.0,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
/** One frame, relative to the calibrated zero. */
|
|
81
|
+
data class MagnetReading(
|
|
82
|
+
var available: Boolean = false,
|
|
83
|
+
var calibrated: Boolean = false,
|
|
84
|
+
var calibrating: Boolean = false,
|
|
85
|
+
var tooClose: Boolean = false,
|
|
86
|
+
var presence: Double = 1.0,
|
|
87
|
+
var offsetEstimate: Vec3 = Vec3.ZERO,
|
|
88
|
+
/** Raw field, µT, device axes. */
|
|
89
|
+
var raw: Vec3 = Vec3.ZERO,
|
|
90
|
+
/** Low-passed field minus the zero, µT. */
|
|
91
|
+
var delta: Vec3 = Vec3.ZERO,
|
|
92
|
+
var strength: Double = 0.0,
|
|
93
|
+
/** 0…1, log scale of `strength`. */
|
|
94
|
+
var pull: Double = 0.0,
|
|
95
|
+
/** Unit vector on screen from the sensor toward the magnet; zero when unknown. */
|
|
96
|
+
var toward: Pt = Pt.ZERO,
|
|
97
|
+
var polarity: Double = 1.0,
|
|
98
|
+
var distance: Double = Double.POSITIVE_INFINITY,
|
|
99
|
+
var moving: Boolean = false,
|
|
100
|
+
/** With the locator: where the magnet is, dp (smoothed), its 1-σ spread, dp, and height above the glass, cm. */
|
|
101
|
+
var screenPosition: Pt? = null,
|
|
102
|
+
var screenSpread: Double = 0.0,
|
|
103
|
+
var height: Double? = null,
|
|
104
|
+
var approachRate: Double = 0.0,
|
|
105
|
+
var approachSpeed: Double = 0.0,
|
|
106
|
+
var lateralVelocity: Pt = Pt.ZERO,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The phone's magnetometer as a magnet detector — the Android twin of
|
|
111
|
+
* MagnetKit's MagnetSource. It reads the uncalibrated magnetometer (Android's
|
|
112
|
+
* own calibration would slowly take a nearby magnet for part of the phone),
|
|
113
|
+
* keeps its own zero, and runs the magnet model. Call [reading] once per frame.
|
|
114
|
+
*/
|
|
115
|
+
class MagnetSource private constructor(private val app: Context) : SensorEventListener {
|
|
116
|
+
companion object {
|
|
117
|
+
@Volatile private var instance: MagnetSource? = null
|
|
118
|
+
fun get(context: Context): MagnetSource = instance ?: synchronized(this) {
|
|
119
|
+
instance ?: MagnetSource(context.applicationContext).also { instance = it }
|
|
120
|
+
}
|
|
121
|
+
private const val SETTLE_DELAY = 0.25
|
|
122
|
+
private const val CAL_DURATION = 0.35
|
|
123
|
+
private const val ZEROS_KEY = "zeros"
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
var config = MagnetConfig()
|
|
127
|
+
/** Feed a raw field by hand, µT in device axes (emulator, tests). */
|
|
128
|
+
var simulatedField: Vec3? = null
|
|
129
|
+
var simulatedAmbient: Vec3? = null
|
|
130
|
+
/** Pause the drift re-zero. */
|
|
131
|
+
var holdBaseline = false
|
|
132
|
+
|
|
133
|
+
private val sensors = app.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
|
134
|
+
private val magSensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED)
|
|
135
|
+
?: sensors.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
|
136
|
+
private val poseSensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_GAME_ROTATION_VECTOR)
|
|
137
|
+
private val gyroSensor: Sensor? = sensors.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
|
138
|
+
val isSensorAvailable: Boolean get() = magSensor != null
|
|
139
|
+
val model: MagnetModel? by lazy { MagnetModel.bundled(app) }
|
|
140
|
+
|
|
141
|
+
@Volatile private var latestRaw: Vec3? = null
|
|
142
|
+
@Volatile private var latestPose: FloatArray? = null
|
|
143
|
+
@Volatile private var gyroRate = 0.0
|
|
144
|
+
|
|
145
|
+
private var subscribers = 0
|
|
146
|
+
private var baseline: Vec3? = null
|
|
147
|
+
private var calStart = -1.0
|
|
148
|
+
private var calSum = Vec3.ZERO
|
|
149
|
+
private var calCount = 0
|
|
150
|
+
private var calPose: FloatArray? = null
|
|
151
|
+
private var smoothed: Vec3? = null
|
|
152
|
+
private var lastSign = 1.0
|
|
153
|
+
private var lastToward = Pt.ZERO
|
|
154
|
+
private var lastPolarity = 1.0
|
|
155
|
+
private var located: Pt? = null
|
|
156
|
+
private val targets = ArrayDeque<Pair<Double, Pt>>()
|
|
157
|
+
private var locatedDistance: Double? = null
|
|
158
|
+
private val lnHistory = ArrayDeque<Triple<Double, Double, Double>>()
|
|
159
|
+
private val posHistory = ArrayDeque<Pair<Double, Pt>>()
|
|
160
|
+
private var motionRate = 0.0
|
|
161
|
+
private val pullHistory = ArrayDeque<Pair<Double, Double>>()
|
|
162
|
+
private var pushed = false
|
|
163
|
+
private var lastPushT = -10.0
|
|
164
|
+
private var lastPullT = -10.0
|
|
165
|
+
private var lastSwipeT = -10.0
|
|
166
|
+
private val listeners = LinkedHashMap<Int, (MagnetGesture) -> Unit>()
|
|
167
|
+
private var nextListenerId = 1
|
|
168
|
+
private val main = Handler(Looper.getMainLooper())
|
|
169
|
+
private var lastT = 0.0
|
|
170
|
+
private var overloadPeak = 0.0
|
|
171
|
+
private var overloadAt = -10.0
|
|
172
|
+
private var leftoverWatchUntil = -10.0
|
|
173
|
+
private var stillSince = -1.0
|
|
174
|
+
private val recent = ArrayDeque<Pair<Double, Vec3>>()
|
|
175
|
+
private var quietSince = -1.0
|
|
176
|
+
private var phantomSince = -1.0
|
|
177
|
+
private val rawHistory = ArrayList<Pair<Double, Vec3>>()
|
|
178
|
+
private var presence = 1.0
|
|
179
|
+
private var offsetEstimate = Vec3.ZERO
|
|
180
|
+
private var presenceAt = 0.0
|
|
181
|
+
/** When the phone was last overloaded, while it has not been zeroed since. */
|
|
182
|
+
private var overloadedAt: Double? = null
|
|
183
|
+
private val recentSize = ArrayDeque<Pair<Double, Double>>()
|
|
184
|
+
private var tapP = 0.0
|
|
185
|
+
private var tapAt = 0.0
|
|
186
|
+
private var tapHigh = false
|
|
187
|
+
private var lastTapT = -10.0
|
|
188
|
+
private val tapPoints = ArrayDeque<Triple<Double, Pt, Double>>()
|
|
189
|
+
private var zeroBeforePhantom: Vec3? = null
|
|
190
|
+
private var zeroBeforePhantomAt = 0.0
|
|
191
|
+
private val prefs = app.getSharedPreferences("magnetkit", Context.MODE_PRIVATE)
|
|
192
|
+
private val knownZeros = loadZeros()
|
|
193
|
+
private var cachedNow = -1.0
|
|
194
|
+
private var cached = MagnetReading()
|
|
195
|
+
|
|
196
|
+
// MARK: Sensors
|
|
197
|
+
|
|
198
|
+
fun attach() {
|
|
199
|
+
subscribers += 1
|
|
200
|
+
if (subscribers != 1) return
|
|
201
|
+
magSensor?.let { sensors.registerListener(this, it, 10_000) }
|
|
202
|
+
poseSensor?.let { sensors.registerListener(this, it, 16_000) }
|
|
203
|
+
gyroSensor?.let { sensors.registerListener(this, it, 16_000) }
|
|
204
|
+
if (baseline == null) calibrate()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fun detach() {
|
|
208
|
+
subscribers = max(0, subscribers - 1)
|
|
209
|
+
if (subscribers != 0) return
|
|
210
|
+
sensors.unregisterListener(this)
|
|
211
|
+
smoothed = null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
override fun onSensorChanged(e: SensorEvent) {
|
|
215
|
+
when (e.sensor.type) {
|
|
216
|
+
Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED, Sensor.TYPE_MAGNETIC_FIELD ->
|
|
217
|
+
latestRaw = Vec3(e.values[0].toDouble(), e.values[1].toDouble(), e.values[2].toDouble())
|
|
218
|
+
Sensor.TYPE_GAME_ROTATION_VECTOR -> {
|
|
219
|
+
val q = FloatArray(4); SensorManager.getQuaternionFromVector(q, e.values); latestPose = q
|
|
220
|
+
}
|
|
221
|
+
Sensor.TYPE_GYROSCOPE -> gyroRate = sqrt((e.values[0] * e.values[0] + e.values[1] * e.values[1] + e.values[2] * e.values[2]).toDouble())
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
226
|
+
|
|
227
|
+
/** Be told about quick movements (push, pull away, tap, swipe), on the main thread. Returns an id to remove it. */
|
|
228
|
+
fun addGestureListener(l: (MagnetGesture) -> Unit): Int { val id = nextListenerId++; listeners[id] = l; return id }
|
|
229
|
+
fun removeGestureListener(id: Int) { listeners.remove(id) }
|
|
230
|
+
private fun fire(g: MagnetGesture) { val ls = listeners.values.toList(); main.post { ls.forEach { it(g) } } }
|
|
231
|
+
|
|
232
|
+
/** Take a new zero over the next ~0.6 s. Keep magnets away. */
|
|
233
|
+
fun calibrate() {
|
|
234
|
+
zeroBeforePhantom = null
|
|
235
|
+
calStart = 0.0
|
|
236
|
+
calSum = Vec3.ZERO
|
|
237
|
+
calCount = 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// MARK: Remembered zeros
|
|
241
|
+
|
|
242
|
+
private fun loadZeros(): MutableList<Pair<Vec3, Double>> {
|
|
243
|
+
val now = System.currentTimeMillis() / 1000.0
|
|
244
|
+
val out = ArrayList<Pair<Vec3, Double>>()
|
|
245
|
+
val a = runCatching { JSONArray(prefs.getString(ZEROS_KEY, "[]")) }.getOrNull() ?: return out
|
|
246
|
+
for (i in 0 until a.length()) {
|
|
247
|
+
val e = a.optJSONArray(i) ?: continue
|
|
248
|
+
if (e.length() == 4 && now - e.getDouble(3) < 2 * 86400) out.add(Pair(Vec3(e.getDouble(0), e.getDouble(1), e.getDouble(2)), e.getDouble(3)))
|
|
249
|
+
}
|
|
250
|
+
return out
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private fun rememberZero(z: Vec3) {
|
|
254
|
+
val now = System.currentTimeMillis() / 1000.0
|
|
255
|
+
knownZeros.removeAll { (it.first - z).length < 2 || now - it.second > 2 * 86400 }
|
|
256
|
+
knownZeros.add(Pair(z, now))
|
|
257
|
+
while (knownZeros.size > 6) knownZeros.removeAt(0)
|
|
258
|
+
val a = JSONArray()
|
|
259
|
+
knownZeros.forEach { a.put(JSONArray(listOf(it.first.x, it.first.y, it.first.z, it.second))) }
|
|
260
|
+
prefs.edit().putString(ZEROS_KEY, a.toString()).apply()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// MARK: Locator
|
|
264
|
+
|
|
265
|
+
private fun screenVector(x: Double, y: Double) = Pt(x, -y) // portrait: device y up → screen y down
|
|
266
|
+
|
|
267
|
+
private fun locate(d: Vec3, cfg: MagnetConfig): Pair<Vec3, Double>? {
|
|
268
|
+
val engine = engineLocate(d, cfg)
|
|
269
|
+
val desk = if (cfg.deskPrior) deskPlacement(d, cfg) else null
|
|
270
|
+
if (desk == null) return engine
|
|
271
|
+
if (engine == null) return Pair(desk.first, desk.second)
|
|
272
|
+
val w = desk.third
|
|
273
|
+
return Pair(engine.first * (1 - w) + desk.first * w, engine.second * (1 - w) + desk.second * w)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A magnet resting at desk height beside the phone: placed beside the edge its sideways field points to. */
|
|
277
|
+
private fun deskPlacement(d: Vec3, cfg: MagnetConfig): Triple<Vec3, Double, Double>? {
|
|
278
|
+
val face = cfg.magnetFace ?: return null
|
|
279
|
+
val sensor = cfg.sensorPosition ?: return null
|
|
280
|
+
val screen = cfg.screenSize ?: return null
|
|
281
|
+
val nb = d.length
|
|
282
|
+
if (nb <= 1e-6) return null
|
|
283
|
+
val f = if (face < 0) -1.0 else 1.0
|
|
284
|
+
if (f * d.z >= 0) return null
|
|
285
|
+
val inPlane = sqrt(d.x * d.x + d.y * d.y) / nb
|
|
286
|
+
val w = min(1.0, max(0.0, (0.3 - inPlane) / 0.15))
|
|
287
|
+
if (w <= 0) return null
|
|
288
|
+
val r = ((cfg.magnetStrength ?: 30000.0) / nb).pow(1.0 / 3.0)
|
|
289
|
+
var tx = f * d.x; var ty = f * d.y
|
|
290
|
+
val tl = sqrt(tx * tx + ty * ty)
|
|
291
|
+
if (tl <= 1e-6) return null
|
|
292
|
+
tx /= tl; ty /= tl
|
|
293
|
+
val ppcm = cfg.pointsPerCm
|
|
294
|
+
val left = -sensor.x / ppcm; val right = (screen.x - sensor.x) / ppcm
|
|
295
|
+
val top = sensor.y / ppcm
|
|
296
|
+
val gap = 1.5
|
|
297
|
+
var x: Double; var y: Double
|
|
298
|
+
if (ty > 0.85) {
|
|
299
|
+
y = top + gap
|
|
300
|
+
if (r <= y) { y = r; x = 0.0 } else x = (if (tx >= 0) 1.0 else -1.0) * sqrt(r * r - y * y)
|
|
301
|
+
} else if (ty < -0.85) {
|
|
302
|
+
x = r * tx; y = r * ty
|
|
303
|
+
} else {
|
|
304
|
+
val side = if (tx >= 0) 1.0 else -1.0
|
|
305
|
+
x = if (side > 0) right + gap else left - gap
|
|
306
|
+
if (r <= abs(x)) { x = side * r; y = 0.0 } else y = -sqrt(r * r - x * x)
|
|
307
|
+
}
|
|
308
|
+
return Triple(Vec3(x, y, 0.5), 0.3 + 0.15 * r, w)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun engineLocate(d: Vec3, cfg: MagnetConfig): Pair<Vec3, Double>? {
|
|
312
|
+
val face = cfg.magnetFace
|
|
313
|
+
if (cfg.locatorMode == MagnetLocatorMode.DIPOLE && face != null) {
|
|
314
|
+
MagnetDipole.locate(d, face, cfg.magnetStrength ?: 30000.0, cfg.dipoleMaxHeight)?.let { (p, inPlane) ->
|
|
315
|
+
val lateral = sqrt(p.x * p.x + p.y * p.y)
|
|
316
|
+
val bearingSD = min(1.2, 0.12 / max(0.05, inPlane))
|
|
317
|
+
return Pair(p, 0.3 + 0.12 * p.length + lateral * bearingSD)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return model?.locator?.locate(d, cfg.magnetStrength, cfg.magnetFace)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// MARK: Reading
|
|
324
|
+
|
|
325
|
+
/** The current reading. Evaluated once per frame (`now` in seconds); every view shares it. */
|
|
326
|
+
fun reading(now: Double): MagnetReading {
|
|
327
|
+
if (now == cachedNow) return cached
|
|
328
|
+
cachedNow = now
|
|
329
|
+
cached = compute(now)
|
|
330
|
+
return cached
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private fun resample(now: Double, n: Int, hz: Double, zero: Vec3): List<Vec3> {
|
|
334
|
+
val h = ArrayList<Vec3>(n)
|
|
335
|
+
var j = 0
|
|
336
|
+
for (k in 0 until n) {
|
|
337
|
+
val tk = now - (n - 1 - k) / hz
|
|
338
|
+
while (j + 1 < rawHistory.size && rawHistory[j + 1].first <= tk) j++
|
|
339
|
+
h.add(rawHistory[j].second - zero)
|
|
340
|
+
}
|
|
341
|
+
return h
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private fun compute(now: Double): MagnetReading {
|
|
345
|
+
val out = MagnetReading()
|
|
346
|
+
val raw = simulatedField ?: latestRaw ?: return out
|
|
347
|
+
out.available = true
|
|
348
|
+
out.raw = raw
|
|
349
|
+
val cfg = config
|
|
350
|
+
val dt = if (lastT > 0) min(0.1, max(0.001, now - lastT)) else 1.0 / 60
|
|
351
|
+
lastT = now
|
|
352
|
+
|
|
353
|
+
var s = smoothed ?: raw
|
|
354
|
+
s += (raw - s) * (1 - exp(-dt / max(0.005, cfg.smoothing)))
|
|
355
|
+
smoothed = s
|
|
356
|
+
|
|
357
|
+
// Calibration: let the reading settle, then average.
|
|
358
|
+
if (calStart >= 0) {
|
|
359
|
+
if (calStart == 0.0) calStart = now
|
|
360
|
+
out.calibrating = true
|
|
361
|
+
out.calibrated = baseline != null
|
|
362
|
+
val age = now - calStart
|
|
363
|
+
if (age >= SETTLE_DELAY) {
|
|
364
|
+
calSum += if (simulatedField != null) (simulatedAmbient ?: raw) else raw
|
|
365
|
+
calCount += 1
|
|
366
|
+
}
|
|
367
|
+
if (age >= SETTLE_DELAY + CAL_DURATION && calCount > 0) {
|
|
368
|
+
val b = calSum / calCount.toDouble()
|
|
369
|
+
baseline = b
|
|
370
|
+
overloadedAt = null
|
|
371
|
+
if (simulatedField == null) rememberZero(b)
|
|
372
|
+
smoothed = b
|
|
373
|
+
calPose = latestPose?.copyOf()
|
|
374
|
+
calStart = -1.0
|
|
375
|
+
stillSince = -1.0
|
|
376
|
+
}
|
|
377
|
+
return out
|
|
378
|
+
}
|
|
379
|
+
val base = baseline ?: run { calibrate(); return out }
|
|
380
|
+
out.calibrated = true
|
|
381
|
+
|
|
382
|
+
// New pose (picked up, turned): re-zero once the phone settles.
|
|
383
|
+
val ref = calPose; val pose = latestPose
|
|
384
|
+
if (simulatedField == null && cfg.autoRecalibrate && ref != null && pose != null) {
|
|
385
|
+
val dot = abs(ref[0] * pose[0] + ref[1] * pose[1] + ref[2] * pose[2] + ref[3] * pose[3]).toDouble()
|
|
386
|
+
val angle = 2 * acos(min(1.0, dot)) * 180 / Math.PI
|
|
387
|
+
if (angle > cfg.poseTolerance) {
|
|
388
|
+
out.moving = true
|
|
389
|
+
if (gyroRate < 0.12) {
|
|
390
|
+
if (stillSince < 0) stillSince = now
|
|
391
|
+
if (now - stillSince > cfg.settleSeconds) calibrate()
|
|
392
|
+
} else stillSince = -1.0
|
|
393
|
+
} else stillSince = -1.0
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Drift and leftovers.
|
|
397
|
+
recent.addLast(Pair(now, s))
|
|
398
|
+
while (recent.isNotEmpty() && now - recent.first().first > 0.8) recent.removeFirst()
|
|
399
|
+
var zero = base
|
|
400
|
+
zeroBeforePhantom?.let { prev ->
|
|
401
|
+
if (now - zeroBeforePhantomAt > 120) zeroBeforePhantom = null
|
|
402
|
+
else if ((s - prev).length < max(2.0, 0.05 * (prev - zero).length)) { zero = prev; baseline = prev; zeroBeforePhantom = null }
|
|
403
|
+
}
|
|
404
|
+
val away = (s - zero).length
|
|
405
|
+
val weak = away < (cfg.driftThreshold ?: max(5.0, 0.08 * cfg.fullScale))
|
|
406
|
+
if (away > cfg.tooCloseStrength) {
|
|
407
|
+
overloadPeak = max(overloadPeak, away); overloadAt = now
|
|
408
|
+
overloadedAt = now
|
|
409
|
+
} else if (overloadPeak > 0) {
|
|
410
|
+
if (away < 0.15 * overloadPeak && now - overloadAt < 20) { leftoverWatchUntil = now + 10; overloadPeak = 0.0 }
|
|
411
|
+
else if (now - overloadAt > 20) overloadPeak = 0.0
|
|
412
|
+
}
|
|
413
|
+
var devMax = Double.POSITIVE_INFINITY
|
|
414
|
+
if (recent.size >= 4) {
|
|
415
|
+
var mean = Vec3.ZERO
|
|
416
|
+
recent.forEach { mean += it.second }
|
|
417
|
+
mean /= recent.size.toDouble()
|
|
418
|
+
devMax = recent.maxOf { (it.second - mean).length }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// The model: presence (~30 Hz) and taps (~50 Hz) from the recent raw field.
|
|
422
|
+
val net = if (cfg.presenceModel) model?.presence else null
|
|
423
|
+
val tapNet = if (cfg.tapModel) model?.tap else null
|
|
424
|
+
if (net != null || tapNet != null) {
|
|
425
|
+
rawHistory.add(Pair(now, raw))
|
|
426
|
+
val keep = max(net?.let { it.window + 1 } ?: 0.0, tapNet?.let { it.window / it.hz + 0.5 } ?: 0.0)
|
|
427
|
+
var cut = 0
|
|
428
|
+
while (cut < rawHistory.size && now - rawHistory[cut].first > keep) cut++
|
|
429
|
+
if (cut > 0) rawHistory.subList(0, cut).clear()
|
|
430
|
+
}
|
|
431
|
+
if (tapNet != null && now - tapAt >= 1.0 / 50) {
|
|
432
|
+
tapAt = now
|
|
433
|
+
tapP = tapNet.evaluate(resample(now, tapNet.window, tapNet.hz, zero))
|
|
434
|
+
} else if (tapNet == null) tapP = 0.0
|
|
435
|
+
if (net != null) {
|
|
436
|
+
if (now - presenceAt >= 1.0 / 30) {
|
|
437
|
+
presenceAt = now
|
|
438
|
+
val h = resample(now, (net.window * net.hz).toInt(), net.hz, zero)
|
|
439
|
+
// Told about an unzeroed overload only while the field has just dropped.
|
|
440
|
+
val size = (raw - zero).length
|
|
441
|
+
recentSize.addLast(Pair(now, size))
|
|
442
|
+
while (recentSize.isNotEmpty() && now - recentSize.first().first > 10) recentSize.removeFirst()
|
|
443
|
+
val peak = recentSize.maxOf { it.second }
|
|
444
|
+
val dropped = peak - size > max(15.0, 0.12 * peak)
|
|
445
|
+
val r = net.evaluate(h, if (dropped) overloadedAt?.let { now - it } else null)
|
|
446
|
+
presence += (r.first - presence) * (1 - exp(-(1.0 / 30) / 0.1))
|
|
447
|
+
offsetEstimate = r.second
|
|
448
|
+
}
|
|
449
|
+
} else presence = 1.0
|
|
450
|
+
|
|
451
|
+
// Settled back onto a remembered no-magnet reading: that is the zero.
|
|
452
|
+
var snapped = false
|
|
453
|
+
if (!holdBaseline && !weak && devMax < 1.5) {
|
|
454
|
+
val k = knownZeros.minByOrNull { (it.first - s).length }
|
|
455
|
+
if (k != null && (k.first - s).length < 2.5) {
|
|
456
|
+
zero = k.first; baseline = k.first
|
|
457
|
+
zeroBeforePhantom = null
|
|
458
|
+
overloadedAt = null
|
|
459
|
+
snapped = true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
val phantom = if (net != null) presence < 0.25 else (cfg.steadyRezero && now < leftoverWatchUntil)
|
|
463
|
+
if (snapped) {
|
|
464
|
+
quietSince = -1.0; phantomSince = -1.0
|
|
465
|
+
} else if (!holdBaseline && weak && devMax < 0.8) {
|
|
466
|
+
phantomSince = -1.0
|
|
467
|
+
if (quietSince < 0) quietSince = now
|
|
468
|
+
if (now - quietSince > 0.6) {
|
|
469
|
+
zero += (s - zero) * (1 - exp(-dt / max(0.2, cfg.driftSeconds)))
|
|
470
|
+
baseline = zero
|
|
471
|
+
}
|
|
472
|
+
} else if (!holdBaseline && phantom && !weak && devMax < max(3.5, 0.03 * away)) {
|
|
473
|
+
// Not a magnet and holding still: the sensor's own offset.
|
|
474
|
+
quietSince = -1.0
|
|
475
|
+
if (phantomSince < 0) phantomSince = now
|
|
476
|
+
if (now - phantomSince > 0.6) {
|
|
477
|
+
if (zeroBeforePhantom == null || now - zeroBeforePhantomAt > 2) { zeroBeforePhantom = zero; zeroBeforePhantomAt = now }
|
|
478
|
+
zero += (s - zero) * (1 - exp(-dt / 0.25))
|
|
479
|
+
baseline = zero
|
|
480
|
+
if ((s - zero).length < 2) overloadedAt = null
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
quietSince = -1.0; phantomSince = -1.0
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
val d = s - zero
|
|
487
|
+
out.delta = d
|
|
488
|
+
val strength = d.length
|
|
489
|
+
out.strength = strength
|
|
490
|
+
out.tooClose = strength > cfg.tooCloseStrength
|
|
491
|
+
val b = max(0.0, strength - cfg.deadzone)
|
|
492
|
+
val kk = max(0.5, cfg.scale)
|
|
493
|
+
out.pull = min(1.0, log10(1 + b / kk) / max(0.01, log10(1 + max(1.0, cfg.fullScale - cfg.deadzone) / kk)))
|
|
494
|
+
out.presence = presence
|
|
495
|
+
out.offsetEstimate = offsetEstimate
|
|
496
|
+
if (net != null) out.pull *= min(1.0, max(0.0, (presence - 0.15) / 0.35))
|
|
497
|
+
if (strength > 1) out.distance = cfg.distanceK * strength.pow(-1.0 / 3.0)
|
|
498
|
+
|
|
499
|
+
// Locator: where the magnet is, smoothed more when unsure.
|
|
500
|
+
val sensor = cfg.sensorPosition
|
|
501
|
+
val loc = if (cfg.locatorEnabled && strength > 2.5 && sensor != null) locate(d, cfg) else null
|
|
502
|
+
if (loc != null && sensor != null) {
|
|
503
|
+
val (pos, spreadCm) = loc
|
|
504
|
+
val v = screenVector(pos.x, pos.y)
|
|
505
|
+
var target = Pt(sensor.x + v.x * cfg.pointsPerCm, sensor.y + v.y * cfg.pointsPerCm)
|
|
506
|
+
val window = if (strength < 15) 0.2 else if (strength < 40) 0.12 else 0.04
|
|
507
|
+
targets.addLast(Pair(now, target))
|
|
508
|
+
while (targets.isNotEmpty() && now - targets.first().first > window) targets.removeFirst()
|
|
509
|
+
if (targets.size >= 3) {
|
|
510
|
+
val xs = targets.map { it.second.x }.sorted(); val ys = targets.map { it.second.y }.sorted()
|
|
511
|
+
target = Pt(xs[xs.size / 2], ys[ys.size / 2])
|
|
512
|
+
}
|
|
513
|
+
val tau = 0.04 + 0.22 * min(1.0, spreadCm / 1.5)
|
|
514
|
+
val cur = located
|
|
515
|
+
located = if (cur != null) cur + (target - cur) * (1 - exp(-dt / tau)) else target
|
|
516
|
+
out.screenPosition = located
|
|
517
|
+
out.screenSpread = spreadCm * cfg.pointsPerCm
|
|
518
|
+
out.height = pos.z - 0.4
|
|
519
|
+
locatedDistance = pos.length
|
|
520
|
+
} else if (strength <= 2.5) {
|
|
521
|
+
located = null; targets.clear(); locatedDistance = null
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Motion: the slope of ln|B| over the last ~0.12 s.
|
|
525
|
+
val rEst = locatedDistance ?: out.distance
|
|
526
|
+
if (strength > max(3.0, cfg.deadzone * 1.5)) lnHistory.addLast(Triple(now, ln(strength), rEst))
|
|
527
|
+
while (lnHistory.isNotEmpty() && now - lnHistory.first().first > 0.12) lnHistory.removeFirst()
|
|
528
|
+
var rate = 0.0
|
|
529
|
+
if (lnHistory.size >= 3) {
|
|
530
|
+
val n = lnHistory.size.toDouble()
|
|
531
|
+
val mt = lnHistory.sumOf { it.first } / n; val ml = lnHistory.sumOf { it.second } / n
|
|
532
|
+
var num = 0.0; var den = 0.0
|
|
533
|
+
lnHistory.forEach { num += (it.first - mt) * (it.second - ml); den += (it.first - mt) * (it.first - mt) }
|
|
534
|
+
if (den > 1e-9) rate = num / den
|
|
535
|
+
}
|
|
536
|
+
motionRate += (rate - motionRate) * (1 - exp(-dt / 0.03))
|
|
537
|
+
out.approachRate = motionRate
|
|
538
|
+
val rNow = lnHistory.lastOrNull()?.third ?: rEst
|
|
539
|
+
out.approachSpeed = if (rNow.isFinite()) motionRate * rNow / 3 else 0.0
|
|
540
|
+
|
|
541
|
+
// Lateral motion of the located point.
|
|
542
|
+
val sp = out.screenPosition
|
|
543
|
+
if (sp != null) {
|
|
544
|
+
posHistory.addLast(Pair(now, sp))
|
|
545
|
+
while (posHistory.isNotEmpty() && now - posHistory.first().first > 0.1) posHistory.removeFirst()
|
|
546
|
+
val first = posHistory.first()
|
|
547
|
+
if (now - first.first > 0.03) {
|
|
548
|
+
val span = now - first.first
|
|
549
|
+
out.lateralVelocity = Pt((sp.x - first.second.x) / span, (sp.y - first.second.y) / span)
|
|
550
|
+
}
|
|
551
|
+
} else posHistory.clear()
|
|
552
|
+
|
|
553
|
+
// Gestures.
|
|
554
|
+
pullHistory.addLast(Pair(now, out.pull))
|
|
555
|
+
while (pullHistory.isNotEmpty() && now - pullHistory.first().first > 0.5) pullHistory.removeFirst()
|
|
556
|
+
val recentPull = pullHistory.maxOfOrNull { it.second } ?: 0.0
|
|
557
|
+
if (motionRate > cfg.gesturePushRate && out.pull >= cfg.gestureMinPull && !pushed && now - lastPushT > 0.3) {
|
|
558
|
+
pushed = true; lastPushT = now
|
|
559
|
+
fire(MagnetGesture.Push(max(0.0, out.approachSpeed)))
|
|
560
|
+
} else if (motionRate < cfg.gesturePushRate * 0.3) pushed = false
|
|
561
|
+
if (motionRate < -cfg.gesturePullRate && recentPull >= cfg.gestureMinPull && now - lastPullT > 0.3) {
|
|
562
|
+
lastPullT = now
|
|
563
|
+
fire(MagnetGesture.PullAway(max(0.0, -out.approachSpeed)))
|
|
564
|
+
if (tapNet == null && now - lastPushT < cfg.gestureTapWindow) fire(MagnetGesture.Tap(out.screenPosition))
|
|
565
|
+
}
|
|
566
|
+
if (tapNet != null) {
|
|
567
|
+
out.screenPosition?.let { tapPoints.addLast(Triple(now, it, strength)) }
|
|
568
|
+
while (tapPoints.isNotEmpty() && now - tapPoints.first().first > 1.0) tapPoints.removeFirst()
|
|
569
|
+
// No presence gate: a tap is a quick rise and fall, which a leftover never makes.
|
|
570
|
+
if (tapP > 0.5 && !tapHigh && now - lastTapT > 0.35 && strength > cfg.deadzone) {
|
|
571
|
+
tapHigh = true; lastTapT = now
|
|
572
|
+
fire(MagnetGesture.Tap(tapPoints.maxByOrNull { it.third }?.second))
|
|
573
|
+
} else if (tapP < 0.3) tapHigh = false
|
|
574
|
+
}
|
|
575
|
+
val lv = out.lateralVelocity
|
|
576
|
+
val lspeed = hypot(lv.x, lv.y)
|
|
577
|
+
if (lspeed > cfg.gestureSwipeSpeed && now - lastSwipeT > 0.4) {
|
|
578
|
+
lastSwipeT = now
|
|
579
|
+
fire(MagnetGesture.Swipe(Pt(lv.x / lspeed, lv.y / lspeed), lspeed))
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Rough side and polarity without the locator.
|
|
583
|
+
if (abs(d.z) > 0.15 * strength + 0.5) lastSign = if (d.z >= 0) 1.0 else -1.0
|
|
584
|
+
val inPlane = hypot(d.x, d.y)
|
|
585
|
+
if (out.pull > 0 && inPlane > 0.06 * strength + 1.5) {
|
|
586
|
+
val flat = abs(d.z) > 0.6 * strength
|
|
587
|
+
val side = if (flat) lastSign else {
|
|
588
|
+
val rv = screenVector(d.x / inPlane, d.y / inPlane)
|
|
589
|
+
if (lastToward == Pt.ZERO) -1.0 else if (rv.x * lastToward.x + rv.y * lastToward.y >= 0) 1.0 else -1.0
|
|
590
|
+
}
|
|
591
|
+
lastToward = screenVector(side * d.x / inPlane, side * d.y / inPlane)
|
|
592
|
+
if (flat) lastPolarity = -lastSign
|
|
593
|
+
} else if (out.pull == 0.0) lastToward = Pt.ZERO
|
|
594
|
+
out.toward = lastToward
|
|
595
|
+
out.polarity = lastPolarity
|
|
596
|
+
return out
|
|
597
|
+
}
|
|
598
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
package dev.libraries.magnetkit
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import kotlin.math.exp
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A small fully connected network stored as MFXL: "MFXL", a little-endian
|
|
8
|
+
* UInt32 header length, a JSON header (`kind`, `layers`: [{in, out, act}],
|
|
9
|
+
* plus whatever the network needs), then each layer's weights (out × in,
|
|
10
|
+
* row-major) and biases as float16. The same files as MagnetKit on iOS.
|
|
11
|
+
*/
|
|
12
|
+
internal class MfxlNet private constructor(
|
|
13
|
+
private val layers: List<Layer>,
|
|
14
|
+
val header: JSONObject,
|
|
15
|
+
) {
|
|
16
|
+
private class Layer(val inN: Int, val outN: Int, val w: FloatArray, val b: FloatArray, val silu: Boolean)
|
|
17
|
+
|
|
18
|
+
val inputs: Int get() = layers.first().inN
|
|
19
|
+
val outputs: Int get() = layers.last().outN
|
|
20
|
+
|
|
21
|
+
fun forward(input: FloatArray): FloatArray {
|
|
22
|
+
var cur = input
|
|
23
|
+
for (l in layers) {
|
|
24
|
+
val dst = FloatArray(l.outN)
|
|
25
|
+
var row = 0
|
|
26
|
+
for (o in 0 until l.outN) {
|
|
27
|
+
var acc = l.b[o]
|
|
28
|
+
for (i in 0 until l.inN) acc += l.w[row + i] * cur[i]
|
|
29
|
+
row += l.inN
|
|
30
|
+
dst[o] = if (l.silu) acc / (1f + exp(-acc)) else acc
|
|
31
|
+
}
|
|
32
|
+
cur = dst
|
|
33
|
+
}
|
|
34
|
+
return cur
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
companion object {
|
|
38
|
+
/** Parses one MFXL network; nil if it is not one of `kind`. */
|
|
39
|
+
fun parse(data: ByteArray, kind: String? = null): MfxlNet? {
|
|
40
|
+
val header = header(data) ?: return null
|
|
41
|
+
if (kind != null && header.optString("kind") != kind && !(kind == "locator" && !header.has("kind"))) return null
|
|
42
|
+
val specs = header.optJSONArray("layers") ?: return null
|
|
43
|
+
var offset = 8 + headerLength(data)
|
|
44
|
+
fun halves(count: Int): FloatArray? {
|
|
45
|
+
if (offset + count * 2 > data.size) return null
|
|
46
|
+
val out = FloatArray(count)
|
|
47
|
+
for (i in 0 until count) {
|
|
48
|
+
val lo = data[offset + i * 2].toInt() and 0xff
|
|
49
|
+
val hi = data[offset + i * 2 + 1].toInt() and 0xff
|
|
50
|
+
out[i] = halfToFloat(lo or (hi shl 8))
|
|
51
|
+
}
|
|
52
|
+
offset += count * 2
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
val layers = ArrayList<Layer>()
|
|
56
|
+
for (k in 0 until specs.length()) {
|
|
57
|
+
val s = specs.getJSONObject(k)
|
|
58
|
+
val inN = s.getInt("in"); val outN = s.getInt("out")
|
|
59
|
+
val w = halves(inN * outN) ?: return null
|
|
60
|
+
val b = halves(outN) ?: return null
|
|
61
|
+
layers.add(Layer(inN, outN, w, b, s.optString("act") == "silu"))
|
|
62
|
+
}
|
|
63
|
+
if (layers.isEmpty()) return null
|
|
64
|
+
return MfxlNet(layers, header)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun headerLength(data: ByteArray): Int =
|
|
68
|
+
(data[4].toInt() and 0xff) or ((data[5].toInt() and 0xff) shl 8) or
|
|
69
|
+
((data[6].toInt() and 0xff) shl 16) or ((data[7].toInt() and 0xff) shl 24)
|
|
70
|
+
|
|
71
|
+
fun header(data: ByteArray): JSONObject? {
|
|
72
|
+
if (data.size <= 8 || data[0] != 'M'.code.toByte() || data[1] != 'F'.code.toByte() ||
|
|
73
|
+
data[2] != 'X'.code.toByte() || data[3] != 'L'.code.toByte()) return null
|
|
74
|
+
val hlen = headerLength(data)
|
|
75
|
+
if (data.size < 8 + hlen) return null
|
|
76
|
+
return runCatching { JSONObject(String(data, 8, hlen, Charsets.UTF_8)) }.getOrNull()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private fun halfToFloat(h: Int): Float {
|
|
80
|
+
val sign = (h shr 15) and 1
|
|
81
|
+
val e = (h shr 10) and 0x1f
|
|
82
|
+
val mant = h and 0x3ff
|
|
83
|
+
val bits = when {
|
|
84
|
+
e == 0 && mant == 0 -> sign shl 31
|
|
85
|
+
e == 0 -> return (if (sign == 1) -1f else 1f) * mant * 5.9604645e-8f // 2^-24
|
|
86
|
+
e == 31 -> (sign shl 31) or 0x7f800000 or (mant shl 13)
|
|
87
|
+
else -> (sign shl 31) or ((e - 15 + 127) shl 23) or (mant shl 13)
|
|
88
|
+
}
|
|
89
|
+
return Float.fromBits(bits)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|